diff --git a/api/bootstrap.php b/api/bootstrap.php
index a0d643b..0547caf 100644
--- a/api/bootstrap.php
+++ b/api/bootstrap.php
@@ -4,23 +4,19 @@
use Auth\Client;
use Auth\Server;
-use GraphQL\Type\Definition\Type as GraphQLType;
use improved\Authors as ImprovedAuthors;
-use Luracast\Restler\Cache\HumanReadable;
+use Luracast\Restler\Data\ErrorResponse;
use Luracast\Restler\Defaults;
use Luracast\Restler\Filters\RateLimiter;
-use Luracast\Restler\GraphQL\GraphQL;
use Luracast\Restler\MediaTypes\Html;
use Luracast\Restler\MediaTypes\Json;
use Luracast\Restler\MediaTypes\Upload;
use Luracast\Restler\MediaTypes\Xml;
use Luracast\Restler\Middleware\SessionMiddleware;
-use Luracast\Restler\Middleware\StaticFiles;
use Luracast\Restler\OpenApi3\Explorer;
use Luracast\Restler\Restler;
use Luracast\Restler\Router;
use Luracast\Restler\UI\Forms;
-use Luracast\Restler\Utils\Text;
use ratelimited\Authors as RateLimitedAuthors;
use SomeVendor\v1\BMI as VendorBMI1;
use v1\BodyMassIndex as BMI1;
@@ -36,11 +32,6 @@
Defaults::$implementations[HttpClientInterface::class] = [SimpleHttpClient::class];
Router::setApiVersion(2);
Html::$template = 'blade'; //'handlebar'; //'twig'; //'php';
-if (!Text::endsWith($_SERVER['SCRIPT_NAME'], 'index.php')) {
- //when serving through apache or nginx, static files will be served direcly by apache / nginx
- Restler::$middleware[] = new StaticFiles(BASE . '/' . 'public');
-}
-//Restler::$middleware[] = new SessionMiddleware('RESTLERSESSID', new ArrayCache(), [0, '', '', false, false]);
Restler::$middleware[] = new SessionMiddleware();
try {
@@ -84,7 +75,7 @@
Router::mapApiClasses(
[
//utility api for running behat tests
- '-storage-' => Storage::class,
+ 'examples/-storage-' => Storage::class,
//examples
'examples/_001_helloworld/say' => Say::class,
'examples/_002_minimal/math' => Math::class,
@@ -115,39 +106,13 @@
//Explorer
'explorer' => Explorer::class,
//GraphQL
- GraphQL::class,
+ //GraphQL::class,
]
);
- //
- //---------------------------- GRAPHQL API ----------------------------
- //
- GraphQL::$queries['echo'] = [
- 'type' => GraphQLType::string(),
- 'args' => [
- 'message' => GraphQLType::nonNull(GraphQLType::string()),
- ],
- 'resolve' => function ($root, $args) {
- return $root['prefix'] . $args['message'];
- }
- ];
- GraphQL::$mutations['sum'] = [
- 'type' => GraphQLType::int(),
- 'args' => [
- 'x' => ['type' => GraphQLType::int()],
- 'y' => ['type' => GraphQLType::int()],
- ],
- 'resolve' => function ($calc, $args) {
- return $args['x'] + $args['y'];
- },
- ];
- GraphQL::mapApiClasses([
- RateLimitedAuthors::class,
- Say::class,
- ]);
- GraphQL::addMethod('', new ReflectionMethod(Math::class, 'add'));
+ require __DIR__ . '/examples/_017_graphql/routes.php';
} catch (Throwable $t) {
- die($t->getMessage());
+ die(json_encode((new ErrorResponse($t, true))->jsonSerialize(), JSON_PRETTY_PRINT));
}
diff --git a/api/common/ReactHttpClient.php b/api/common/ReactHttpClient.php
index 1409ff8..9338cb4 100644
--- a/api/common/ReactHttpClient.php
+++ b/api/common/ReactHttpClient.php
@@ -1,7 +1,9 @@
request($method, $uri, $headers, '1.1');
- $req->on('response', function (Response $response) use ($callback) {
- $body = '';
- $headers = $response->getHeaders();
- $response->on('data', function (string $chunk) use (&$body) {
- $body .= $chunk;
- });
- $response->on('end', function () use (&$body, $headers, $callback) {
- $callback(null, new SimpleHttpResponse($body, $headers));
- });
+ $req = $browser->request($method, $uri, $headers, $body);
+ $req->then(function (ResponseInterface $response) use ($callback) {
+ $callback(null, new SimpleHttpResponse((string)$response->getBody(), $response->getHeaders()));
+ }, function (Exception $exception) use ($callback) {
+ $callback($exception);
});
- $req->end($body);
}
}
diff --git a/api/examples/-storage-/.htaccess b/api/examples/-storage-/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/-storage-/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/tests/Storage.php b/api/examples/-storage-/Storage.php
similarity index 99%
rename from api/tests/Storage.php
rename to api/examples/-storage-/Storage.php
index 1049bf7..7fa3f7f 100644
--- a/api/tests/Storage.php
+++ b/api/examples/-storage-/Storage.php
@@ -111,7 +111,7 @@ class_exists(ClassName::get('HttpClientInterface'));
*
* Removes the cache files to begin testing on a clean slate
*/
- public function delete()
+ public function deleteAll()
{
$this->deleteCache();
$this->deletePackage();
diff --git a/api/examples/-storage-/index.php b/api/examples/-storage-/index.php
new file mode 100644
index 0000000..ce3bef4
--- /dev/null
+++ b/api/examples/-storage-/index.php
@@ -0,0 +1,19 @@
+ Storage::class
+]);
+
+(new Restler())->handle();
diff --git a/api/examples/_001_helloworld/.htaccess b/api/examples/_001_helloworld/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_001_helloworld/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_001_helloworld/Say.php b/api/examples/_001_helloworld/Say.php
index 3269856..358a4b7 100755
--- a/api/examples/_001_helloworld/Say.php
+++ b/api/examples/_001_helloworld/Say.php
@@ -5,7 +5,7 @@
*/
class Say
{
- function hello(string $to = 'world'): string
+ function hello($to = 'world'):string
{
return "Hello $to!";
}
diff --git a/api/examples/_001_helloworld/index.php b/api/examples/_001_helloworld/index.php
new file mode 100644
index 0000000..b531c21
--- /dev/null
+++ b/api/examples/_001_helloworld/index.php
@@ -0,0 +1,12 @@
+handle();
\ No newline at end of file
diff --git a/api/examples/_002_minimal/.htaccess b/api/examples/_002_minimal/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_002_minimal/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_002_minimal/Math.php b/api/examples/_002_minimal/Math.php
index 51e07e1..db76cca 100755
--- a/api/examples/_002_minimal/Math.php
+++ b/api/examples/_002_minimal/Math.php
@@ -23,16 +23,25 @@ function add($n1 = 1, $n2 = 1)
*/
function multiply($n1, $n2)
{
- return array(
+ return [
'result' => ($n1 * $n2)
- );
+ ];
}
/**
* @url GET sum/*
*/
- function sum()
+ function _sum()
{
return array_sum(func_get_args());
}
+
+ /**
+ * @param int ...$numbers {@from path}
+ * @return int
+ */
+ function sum2(int ...$numbers):int
+ {
+ return array_sum($numbers);
+ }
}
diff --git a/api/examples/_002_minimal/index.php b/api/examples/_002_minimal/index.php
new file mode 100644
index 0000000..5260eda
--- /dev/null
+++ b/api/examples/_002_minimal/index.php
@@ -0,0 +1,13 @@
+handle();
\ No newline at end of file
diff --git a/api/examples/_003_multiformat/.htaccess b/api/examples/_003_multiformat/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_003_multiformat/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_003_multiformat/index.php b/api/examples/_003_multiformat/index.php
new file mode 100644
index 0000000..e2a86d9
--- /dev/null
+++ b/api/examples/_003_multiformat/index.php
@@ -0,0 +1,17 @@
+handle();
\ No newline at end of file
diff --git a/api/examples/_004_error_response/.htaccess b/api/examples/_004_error_response/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_004_error_response/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_004_error_response/Currency.php b/api/examples/_004_error_response/Currency.php
index 0bfde23..d43279b 100755
--- a/api/examples/_004_error_response/Currency.php
+++ b/api/examples/_004_error_response/Currency.php
@@ -18,7 +18,6 @@ function format($number = null)
}
// let's format it as US currency
- $m = new NumberFormatter("en-US", NumberFormatter::CURRENCY);
- return $m->format($number);
+ return'$' . number_format($number, 2);
}
}
diff --git a/api/examples/_004_error_response/index.php b/api/examples/_004_error_response/index.php
new file mode 100644
index 0000000..89da566
--- /dev/null
+++ b/api/examples/_004_error_response/index.php
@@ -0,0 +1,13 @@
+handle();
\ No newline at end of file
diff --git a/api/examples/_005_protected_api/.htaccess b/api/examples/_005_protected_api/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_005_protected_api/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_005_protected_api/index.php b/api/examples/_005_protected_api/index.php
new file mode 100644
index 0000000..d4e7d66
--- /dev/null
+++ b/api/examples/_005_protected_api/index.php
@@ -0,0 +1,15 @@
+ Simple::class,
+ Secured::class
+]);
+
+(new Restler())->handle();
diff --git a/api/examples/_006_routing/.htaccess b/api/examples/_006_routing/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_006_routing/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_006_routing/index.php b/api/examples/_006_routing/index.php
new file mode 100644
index 0000000..47153ad
--- /dev/null
+++ b/api/examples/_006_routing/index.php
@@ -0,0 +1,13 @@
+handle();
\ No newline at end of file
diff --git a/api/examples/_007_crud/.htaccess b/api/examples/_007_crud/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_007_crud/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_007_crud/index.php b/api/examples/_007_crud/index.php
new file mode 100644
index 0000000..4635e27
--- /dev/null
+++ b/api/examples/_007_crud/index.php
@@ -0,0 +1,18 @@
+handle();
\ No newline at end of file
diff --git a/api/examples/_008_documentation/.htaccess b/api/examples/_008_documentation/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_008_documentation/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_008_documentation/index.php b/api/examples/_008_documentation/index.php
new file mode 100644
index 0000000..8af311a
--- /dev/null
+++ b/api/examples/_008_documentation/index.php
@@ -0,0 +1,21 @@
+handle();
\ No newline at end of file
diff --git a/api/examples/_009_rate_limiting/.htaccess b/api/examples/_009_rate_limiting/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_009_rate_limiting/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_009_rate_limiting/index.php b/api/examples/_009_rate_limiting/index.php
new file mode 100644
index 0000000..3533fa2
--- /dev/null
+++ b/api/examples/_009_rate_limiting/index.php
@@ -0,0 +1,25 @@
+handle();
diff --git a/api/examples/_010_access_control/.htaccess b/api/examples/_010_access_control/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_010_access_control/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_010_access_control/Access.php b/api/examples/_010_access_control/Access.php
index 3d59dbe..5a992e6 100755
--- a/api/examples/_010_access_control/Access.php
+++ b/api/examples/_010_access_control/Access.php
@@ -53,6 +53,8 @@ public function documents(): array
* @return string
* @throws HttpException 403 permission denied
* @throws HttpException 404 document not found
+ *
+ * @url GET documents/{id}
*/
public function getDocuments(int $id): string
{
diff --git a/api/examples/_010_access_control/AccessControl.php b/api/examples/_010_access_control/AccessControl.php
index 5bb8393..d00b3c0 100755
--- a/api/examples/_010_access_control/AccessControl.php
+++ b/api/examples/_010_access_control/AccessControl.php
@@ -9,7 +9,6 @@
use Luracast\Restler\OpenApi3\Security\ApiKeyAuth;
use Luracast\Restler\OpenApi3\Security\Scheme;
use Luracast\Restler\ResponseHeaders;
-use Luracast\Restler\StaticProperties;
use Luracast\Restler\Utils\ClassName;
use Psr\Http\Message\ServerRequestInterface;
@@ -17,16 +16,25 @@ class AccessControl implements AccessControlInterface, SelectivePathsInterface,
{
use SelectivePathsTrait;
- public $requires = 'user';
- public $role = 'user';
- public $id = null;
-
/** @var string[][] hardcoded to string[password]=>[id,role] for brevity */
private static $users = [
'123' => ['a', 'user'],
'456' => ['b', 'user'],
'789' => ['c', 'admin']
];
+ public $requires = 'user';
+ public $role = 'user';
+ public $id = null;
+
+ public static function getWWWAuthenticateString(): string
+ {
+ return 'Query name="api_key"';
+ }
+
+ public static function scheme(): Scheme
+ {
+ return new ApiKeyAuth('api_key', ApiKeyAuth::IN_QUERY);
+ }
/**
* @param string $owner
@@ -36,9 +44,15 @@ class AccessControl implements AccessControlInterface, SelectivePathsInterface,
*/
public function _verifyPermissionForDocumentOwnedBy(string $owner, bool $throwException = false): bool
{
- if ('admin' === $this->role) return true; //comment this line to make it owner only
- if ($owner === $this->id) return true;
- if (!$throwException) return false;
+ if ('admin' === $this->role) {
+ return true;
+ } //comment this line to make it owner only
+ if ($owner === $this->id) {
+ return true;
+ }
+ if (!$throwException) {
+ return false;
+ }
throw new HttpException(403, 'permission denied.');
}
@@ -50,7 +64,7 @@ public function _verifyPermissionForDocumentOwnedBy(string $owner, bool $throwEx
*/
public function _isAllowed(ServerRequestInterface $request, ResponseHeaders $responseHeaders): bool
{
- if (!$api_key = $request->getQueryParams()['api_key'] ?? false) {
+ if (!$api_key = $request->getQueryParams()['api_key'] ?? $request->getHeaderLine('api_key') ?? false) {
return false;
}
/** @var UserIdentificationInterface $userClass */
@@ -66,14 +80,4 @@ public function _isAllowed(ServerRequestInterface $request, ResponseHeaders $res
//Role-based access control (RBAC)
return $role === 'admin' || $role === $this->requires;
}
-
- public static function getWWWAuthenticateString(): string
- {
- return 'Query name="api_key"';
- }
-
- public static function scheme(): Scheme
- {
- return new ApiKeyAuth('api_key', ApiKeyAuth::IN_QUERY);
- }
}
diff --git a/api/examples/_010_access_control/index.php b/api/examples/_010_access_control/index.php
new file mode 100644
index 0000000..35f9e90
--- /dev/null
+++ b/api/examples/_010_access_control/index.php
@@ -0,0 +1,18 @@
+ Access::class,
+ Explorer::class
+]);
+
+(new Restler())->handle();
\ No newline at end of file
diff --git a/api/examples/_011_versioning/.htaccess b/api/examples/_011_versioning/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_011_versioning/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_011_versioning/index.php b/api/examples/_011_versioning/index.php
new file mode 100644
index 0000000..1c64000
--- /dev/null
+++ b/api/examples/_011_versioning/index.php
@@ -0,0 +1,21 @@
+ BodyMassIndex::class,
+ Explorer::class
+]);
+
+(new Restler())->handle();
\ No newline at end of file
diff --git a/api/examples/_011_versioning/v2/BodyMassIndex.php b/api/examples/_011_versioning/v2/BodyMassIndex.php
index ac718f7..c66735b 100755
--- a/api/examples/_011_versioning/v2/BodyMassIndex.php
+++ b/api/examples/_011_versioning/v2/BodyMassIndex.php
@@ -1,8 +1,30 @@
message = 'Obesity';
}
- $result->metric = array(
+ $result->metric = Unit::__set_state([
'height' => "$cm centimeters",
'weight' => "$weight kilograms"
- );
- $result->imperial = array(
+ ]);
+ $result->imperial = Unit::__set_state([
'height' => "$feet feet $inches inches",
'weight' => "$lb pounds"
- );
+ ]);
return $result;
}
}
diff --git a/api/examples/_012_vendor_mime/.htaccess b/api/examples/_012_vendor_mime/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_012_vendor_mime/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_012_vendor_mime/index.php b/api/examples/_012_vendor_mime/index.php
new file mode 100644
index 0000000..4a38d37
--- /dev/null
+++ b/api/examples/_012_vendor_mime/index.php
@@ -0,0 +1,22 @@
+ BMI::class,
+ Explorer::class
+]);
+
+(new Restler())->handle();
\ No newline at end of file
diff --git a/api/examples/_013_html/.htaccess b/api/examples/_013_html/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_013_html/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_013_html/Tasks.php b/api/examples/_013_html/Tasks.php
index 46165dc..cbc5f19 100644
--- a/api/examples/_013_html/Tasks.php
+++ b/api/examples/_013_html/Tasks.php
@@ -1,5 +1,6 @@
db->delete($id);
}
-}
\ No newline at end of file
+}
diff --git a/api/examples/_013_html/img/button_green.png b/api/examples/_013_html/img/button_green.png
new file mode 100755
index 0000000..3e0691a
Binary files /dev/null and b/api/examples/_013_html/img/button_green.png differ
diff --git a/api/examples/_013_html/img/delete.png b/api/examples/_013_html/img/delete.png
new file mode 100755
index 0000000..08f2493
Binary files /dev/null and b/api/examples/_013_html/img/delete.png differ
diff --git a/api/examples/_013_html/img/edit.png b/api/examples/_013_html/img/edit.png
new file mode 100755
index 0000000..b93e776
Binary files /dev/null and b/api/examples/_013_html/img/edit.png differ
diff --git a/api/examples/_013_html/index.php b/api/examples/_013_html/index.php
new file mode 100644
index 0000000..e0ab7d2
--- /dev/null
+++ b/api/examples/_013_html/index.php
@@ -0,0 +1,25 @@
+handle();
\ No newline at end of file
diff --git a/api/examples/_013_html/script.js b/api/examples/_013_html/script.js
new file mode 100755
index 0000000..6397569
--- /dev/null
+++ b/api/examples/_013_html/script.js
@@ -0,0 +1,155 @@
+$(document).ready(function () {
+ /* The following code is executed once the DOM is loaded */
+
+ $(".todoList").sortable({
+ axis : 'y', // Only vertical movements allowed
+ containment : 'window', // Constrained by the window
+ update : function (event, ui) { // The function is called after the todos are rearranged
+ id = ui.item[0].id;
+
+ // The toArray method returns an array with the ids of the todos
+ var arr = $(".todoList").sortable('toArray');
+
+ $.ajax({
+ url: 'tasks/' + id.replace('todo-', '') + '.json',
+ contentType: 'application/json',
+ type: 'PATCH',
+ dataType: 'json',
+ data: '{"position": ' + arr.indexOf(id) + '}'
+ });
+ },
+
+ /* Opera fix: */
+
+ stop : function (e, ui) {
+ ui.item.css({'top' : '0', 'left' : '0'});
+ }
+ });
+
+ // A global variable, holding a jQuery object
+ // containing the current todo item:
+
+ var currentTODO;
+
+ // Configuring the delete confirmation dialog
+ $("#dialog-confirm").dialog({
+ resizable : false,
+ height : 130,
+ modal : true,
+ autoOpen : false,
+ buttons : {
+ 'Delete item' : function () {
+ $.ajax({
+ url : 'tasks/' + currentTODO.data('id') + '.html',
+ type : 'DELETE',
+ success : function (msg) {
+ currentTODO.fadeOut('fast');
+ }
+ });
+ $(this).dialog('close');
+ },
+ Cancel : function () {
+ $(this).dialog('close');
+ }
+ }
+ });
+
+ // When a double click occurs, just simulate a click on the edit button:
+ $('.todo').live('dblclick', function () {
+ $(this).find('a.edit').click();
+ });
+
+ // If any link in the todo is clicked, assign
+ // the todo item to the currentTODO variable for later use.
+
+ $('.todo a').live('click', function (e) {
+
+ currentTODO = $(this).closest('.todo');
+ currentTODO.data('id', currentTODO.attr('id').replace('todo-', ''));
+
+ e.preventDefault();
+ });
+
+ // Listening for a click on a delete button:
+
+ $('.todo a.delete').live('click', function () {
+ $("#dialog-confirm").dialog('open');
+ });
+
+ // Listening for a click on a edit button
+
+ $('.todo a.edit').live('click', function () {
+
+ var container = currentTODO.find('.text');
+
+ if (!currentTODO.data('origText')) {
+ // Saving the current value of the ToDo so we can
+ // restore it later if the user discards the changes:
+
+ currentTODO.data('origText', container.text());
+ }
+ else {
+ // This will block the edit button if the edit box is already open:
+ return false;
+ }
+
+ $('').val(container.text()).appendTo(container.empty());
+
+ // Appending the save and cancel links:
+ container.append(
+ '
'
+ );
+
+ });
+
+ // The cancel edit link:
+
+ $('.todo a.discardChanges').live('click', function () {
+ currentTODO.find('.text')
+ .text(currentTODO.data('origText'))
+ .end()
+ .removeData('origText');
+ });
+
+ // The save changes link:
+
+ $('.todo a.saveChanges').live('click', function () {
+ var text = currentTODO.find("input[type=text]").val();
+
+ $.ajax({
+ url: 'tasks/' + currentTODO.data('id') + '.json',
+ contentType: 'application/json',
+ type: 'PATCH',
+ dataType: 'json',
+ data: '{"text": "' + text + '"}'
+ });
+
+ currentTODO.removeData('origText')
+ .find(".text")
+ .text(text);
+ });
+
+
+ // The Add New ToDo button:
+
+ var timestamp = 0;
+ $('#addButton').click(function (e) {
+
+ // Only one todo per 5 seconds is allowed:
+ if ((new Date()).getTime() - timestamp < 5000) return false;
+
+ $.post("tasks.html", {'text' : 'New Task. Double Click to Edit.', 'rand' : Math.random()}, function (msg) {
+
+ // Appending the new todo and fading it into view:
+ $(msg).hide().appendTo('.todoList').fadeIn();
+ });
+
+ // Updating the timestamp:
+ timestamp = (new Date()).getTime();
+
+ e.preventDefault();
+ });
+
+}); // Closing $(document).ready()
\ No newline at end of file
diff --git a/api/examples/_013_html/styles.css b/api/examples/_013_html/styles.css
new file mode 100755
index 0000000..3ec8b1d
--- /dev/null
+++ b/api/examples/_013_html/styles.css
@@ -0,0 +1,202 @@
+*{
+ /* Resetting the default styles of the page */
+ margin:0;
+ padding:0;
+}
+
+body{
+ /* Setting default text color, background and a font stack */
+ font-size:0.825em;
+ color:#666;
+ background-color:#fff;
+ font-family:Arial, Helvetica, sans-serif;
+}
+
+/* The todo items are grouped into an UL unordered list */
+
+ul.todoList{
+ margin:0 auto;
+ width:500px;
+ position:relative;
+}
+
+ul.todoList li{
+ background-color:#F9F9F9;
+ border:1px solid #EEEEEE;
+ list-style:none;
+ margin:6px;
+ padding:6px 9px;
+ position:relative;
+ cursor:n-resize;
+
+ /* CSS3 text shadow and rounded corners: */
+
+ text-shadow:1px 1px 0 white;
+
+ -moz-border-radius:6px;
+ -webkit-border-radius:6px;
+ border-radius:6px;
+}
+
+ul.todoList li:hover{
+ border-color:#9be0f9;
+
+ /* CSS3 glow effect: */
+ -moz-box-shadow:0 0 5px #A6E5FD;
+ -webkit-box-shadow:0 0 5px #A6E5FD;
+ box-shadow:0 0 5px #A6E5FD;
+}
+
+.todo .text{
+ color:#777777;
+ font-size:1.4em;
+}
+
+/* The edit and delete buttons */
+
+.todo .actions{
+ position:absolute;
+ right:7px;
+ top:6px;
+}
+
+.todo .actions a{
+ display:block;
+ width:16px;
+ height:16px;
+ overflow:hidden;
+ float:left;
+ text-indent:-9999px;
+ margin:3px;
+}
+
+.todo .actions a.edit{
+ background:url("img/edit.png") no-repeat center center;
+}
+
+.todo .actions a.delete{
+ background:url("img/delete.png") no-repeat center center;
+}
+
+/* The edit textbox */
+
+.todo input{
+ border:1px solid #CCCCCC;
+ color:#666666;
+ font-family:Arial,Helvetica,sans-serif;
+ font-size:0.725em;
+ padding:3px 4px;
+ width:300px;
+}
+
+/* The Save and Cancel edit links: */
+
+.editTodo{
+ display:inline;
+ font-size:0.6em;
+ padding-left:9px;
+}
+
+.editTodo a{
+ font-weight:bold;
+}
+
+a.discardChanges{
+ color:#C00 !important;
+}
+
+a.saveChanges{
+ color:#4DB209 !important;
+}
+
+/* Overwriting some of the default jQuery UI styles */
+
+.ui-button,.ui-dialog-titlebar{
+ font-size:0.72em !important;
+}
+
+#dialog-confirm{
+ display:none;
+ font-size:0.9em;
+ padding:1em 1em 0;
+}
+
+#addButton{
+ margin:20px auto;
+}
+
+
+/* Green button class: */
+
+a.green-button,
+a.green-button:visited{
+ color:black;
+ display:block;
+ font-size:10px;
+ font-weight:bold;
+ height:15px;
+ padding:6px 5px 4px;
+ text-align:center;
+ width:60px;
+
+ text-shadow:1px 1px 1px #DDDDDD;
+ background:url("img/button_green.png") no-repeat left top;
+}
+
+a.green-button:hover{
+ text-decoration:none;
+ background-position:left bottom;
+}
+
+
+/* The styles below are only necessary for the styling of the demo page: */
+
+#main{
+ position:relative;
+ margin:0 auto;
+ width:960px;
+}
+
+h1{
+ padding:30px 0;
+ text-align:center;
+ text-shadow:0 1px 1px white;
+ margin-bottom:30px;
+ background-color:#f8f8f8;
+ font-size:26px;
+}
+
+h1,h2{
+ font-family:"Myriad Pro",Arial,Helvetica,sans-serif;
+}
+
+h2{
+ font-size:14px;
+ font-weight:normal;
+ text-align:center;
+
+ position:absolute;
+ right:40px;
+ top:40px;
+}
+
+.note{
+ font-size:12px;
+ font-style:italic;
+ padding-bottom:20px;
+ text-align:center;
+}
+
+a, a:visited {
+ color:#0196e3;
+ text-decoration:none;
+ outline:none;
+}
+
+a:hover{
+ text-decoration:underline;
+}
+
+a img{
+ border:none;
+}
\ No newline at end of file
diff --git a/api/examples/_014_oauth2_client/.htaccess b/api/examples/_014_oauth2_client/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_014_oauth2_client/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_014_oauth2_client/Auth/Client.php b/api/examples/_014_oauth2_client/Auth/Client.php
index 3c0464e..75d414b 100644
--- a/api/examples/_014_oauth2_client/Auth/Client.php
+++ b/api/examples/_014_oauth2_client/Auth/Client.php
@@ -45,7 +45,8 @@ public function __construct(Restler $restler, SessionInterface $session, StaticP
$session->start();
$this->html->data['session_id'] = $session->getId();
if (!static::$serverUrl) {
- $base = (string)$this->restler->baseUrl . '/examples/';
+ $path = rtrim($restler->baseUrl, '/') . '/' . $restler->path;
+ $base = explode('_014_oauth2_client', $path)[0];
static::$serverUrl =
$base . '_015_oauth2_server';
static::$replyBackUrl = $base . '_014_oauth2_client/authorized';
@@ -78,10 +79,10 @@ private function fullURL($path)
*/
public function index()
{
- return array(
+ return [
'authorize_url' => static::$authorizeRoute,
'authorize_redirect_url' => static::$replyBackUrl
- );
+ ];
}
/**
@@ -117,16 +118,16 @@ public function authorized(
// the user denied the authorization request
if (!$code) {
$this->html->view = 'oauth2/client/denied.twig';
- return array('error' => compact('error_description', 'error_uri'));
+ return ['error' => compact('error_description', 'error_uri')];
}
// exchange authorization code for access token
- $query = array(
+ $query = [
'grant_type' => 'authorization_code',
'code' => $code,
'client_id' => static::$clientId,
'client_secret' => static::$clientSecret,
'redirect_uri' => static::$replyBackUrl,
- );
+ ];
/** @var HttpClientInterface $clientClass */
$clientClass = ClassName::get(HttpClientInterface::class);
try {
diff --git a/api/examples/_014_oauth2_client/css/demo.css b/api/examples/_014_oauth2_client/css/demo.css
new file mode 100644
index 0000000..59b5220
--- /dev/null
+++ b/api/examples/_014_oauth2_client/css/demo.css
@@ -0,0 +1,557 @@
+article, aside, footer, header, nav, section {
+ display: block; }
+
+.group:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden; }
+
+* {
+ margin: 0;
+ padding: 0; }
+
+body {
+ font-size: 0.924em;
+ line-height: 1.75em;
+ color: #313126;
+ font-family: "ff-tisa-web-pro-1", "ff-tisa-web-pro-2", Georgia, serif; }
+
+header[role=banner] h1,
+header[role=banner] h2 {
+ margin: 0; }
+header[role=banner] h1 {
+ font-size: 145%;
+ font-weight: 700;
+ font-style: italic;
+ letter-spacing: -1px;
+ text-shadow: 0 2px 5px #dcdbd1; }
+header[role=banner] h2 {
+ color: #87877d;
+ font-size: 0.88em;
+ font-weight: 400; }
+
+h1, h2, h3, #sidebar {
+ font-family: "ff-tisa-web-pro-1", "ff-tisa-web-pro-2", Georgia, serif; }
+
+h1, h2, h3 {
+ font-weight: 400; }
+
+h4, h5, h6 {
+ font-family: "ff-tisa-web-pro-1", "ff-tisa-web-pro-2", Georgia, serif;
+ font-family: Georgia, serif; }
+
+h1 {
+ margin-top: 0.44em;
+ margin-bottom: 0.294em;
+ line-height: 1.606em;
+ font-size: 218.0%; }
+
+h2 {
+ margin-top: 1.463em;
+ margin-bottom: 0.488em;
+ line-height: 1.067em;
+ font-size: 164%; }
+
+h3 {
+ margin-top: 1.655em;
+ margin-bottom: 0.552em;
+ line-height: 1.207em;
+ font-size: 145%; }
+
+h4 {
+ margin-top: 2.712em;
+ margin-bottom: 0em;
+ line-height: 1.483em;
+ font-size: 118%; }
+
+ol,
+p,
+pre,
+ul {
+ margin: 0;
+ margin-bottom: 0.8em; }
+
+li {
+ margin-top: 0em;
+ margin-bottom: 0em;
+ line-height: 1.75em;
+ font-size: 100%; }
+
+blockquote {
+ margin: 1.6em 0;
+ padding: 0 1.6em;
+ font-style: italic;
+ color: #86867b; }
+
+pre {
+ padding: 0.875em 0;
+ overflow: auto; }
+ pre code {
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ overflow: auto;
+ padding-left: 1em; }
+
+img {
+ border: none; }
+
+nav.breadcrumb {
+ margin-top: 1.75em;
+ color: #87877d;
+ padding: 0.5em 0;
+ font-size: 0.909em; }
+
+article[role=main] ol, article[role=main] ul {
+ margin-left: 1.5em; }
+
+div#container {
+ position: relative; }
+
+header[role=banner] {
+ position: relative; }
+ header[role=banner] nav.primary {
+ line-height: 1.75em; }
+ header[role=banner] nav.primary ul.menu {
+ margin: 0.875em 0;
+ list-style: none; }
+ header[role=banner] nav.primary ul.menu:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden; }
+ header[role=banner] nav.primary ul.menu li {
+@adjust-font-size($base-scale);
+ font-weight: 700;
+ width: 48%; }
+ header[role=banner] nav.primary ul.menu li:nth-child(odd) {
+ float: left; }
+ header[role=banner] nav.primary ul.menu li:nth-child(even) {
+ float: right; }
+ header[role=banner] nav.primary ul.menu li a {
+ text-decoration: none; }
+ header[role=banner] p.links {
+ position: absolute;
+ top: 0.7em;
+ left: 8em;
+ margin: 0; }
+ header[role=banner] p.links a {
+ width: 16px;
+ height: 16px;
+ float: left;
+ margin-right: 8px;
+ display: block;
+ text-indent: -9999px; }
+ header[role=banner] p.links a.feed {
+ background: url(/images/feed.png) no-repeat; }
+ header[role=banner] p.links a.twitter {
+ background: url(/images/twitter.png) no-repeat; }
+ header[role=banner] p.links a.github {
+ background: url(/images/github-icon.png) no-repeat;
+ width: 24px;
+ height: 24px;
+ margin-top: -4px; }
+
+div#container {
+ width: 93%;
+ margin: 0 auto;
+ padding: 1em 1em 0 1em; }
+ div#container div#content {
+ position: relative; }
+ div#container footer.branding {
+ clear: both;
+ color: #87877d;
+ margin-top: 1.818em;
+ margin-bottom: 1.818em;
+ line-height: 1.989em;
+ font-size: 88%; }
+ div#container footer.branding p {
+ width: auto;
+ margin: 0;
+ padding: 1em 0; }
+
+a {
+ color: #d85700;
+ -moz-transition: color 0.25s 0 ease;
+ -o-transition: color 0.25s 0 ease;
+ -webkit-transition: color 0.25s 0 ease;
+ transition: color 0.25s 0 ease; }
+ a:visited {
+ color: #bf4d00; }
+ a:hover {
+ color: #ff7d25; }
+ a:active {
+ color: #722e00; }
+
+nav.breadcrumb ul {
+ margin: 0; }
+nav.breadcrumb li {
+ display: inline;
+ list-style: none; }
+ nav.breadcrumb li::after {
+ content: " > "; }
+ nav.breadcrumb li:last-child::after {
+ content: ""; }
+
+nav.breadcrumb a,
+div.feed a,
+article p.meta a {
+ color: #e2a176; }
+nav.breadcrumb:hover a,
+div.feed:hover a,
+article p.meta:hover a {
+ color: #d85700; }
+nav.breadcrumb a:hover,
+div.feed a:hover,
+article p.meta a:hover {
+ color: #ff7d25; }
+
+article img {
+ max-width: 100%;
+ margin-bottom: 1.6em; }
+article footer {
+ border-top: 1px dashed #d7c28b; }
+ article footer p.meta {
+ margin-top: 0.176em;
+ margin-bottom: 3.344em;
+ line-height: 1.925em;
+ font-size: 90.9%;
+ font-style: italic;
+ color: #87877d; }
+
+article[role="main"] h1, article[role="main"] h2 {
+ text-shadow: 0 2px 5px #dcdbd1; }
+article[role="main"] div#disqus_thread img {
+ max-width: none; }
+article[role="main"] div#disqus_thread ul#dsq-comments {
+ margin-left: 0; }
+
+section.pages > ol,
+section.articles > ol {
+ margin-left: 0; }
+ section.pages > ol li,
+ section.articles > ol li {
+ position: relative;
+ margin: 1.6em 0;
+ list-style: none; }
+ section.pages > ol article ol li,
+ section.articles > ol article ol li {
+ list-style: decimal; }
+ section.pages > ol article ul li,
+ section.articles > ol article ul li {
+ list-style: disc; }
+section.pages header[role=main] h1,
+section.articles header[role=main] h1 {
+ margin-top: 1.101em;
+ margin-bottom: 0.367em;
+ line-height: 1.606em;
+ font-size: 218.0%; }
+section.pages header h1,
+section.articles header h1 {
+ margin-top: 1.463em;
+ margin-bottom: 0.488em;
+ line-height: 1.067em;
+ font-size: 164%; }
+section.pages article h1,
+section.articles article h1 {
+ font-weight: normal;
+ text-shadow: none; }
+ section.pages article h1 a,
+ section.articles article h1 a {
+ text-decoration: none; }
+section.pages article p.read_more,
+section.articles article p.read_more {
+ margin-top: 0em;
+ margin-bottom: 0em;
+ line-height: 1.75em;
+ font-size: 100%;
+ margin-top: -0.8em; }
+section.pages article footer,
+section.articles article footer {
+ border-top: none; }
+
+section.pages > ol article p.read_more {
+ margin-top: 0; }
+
+div#sidebar heading h1 {
+ margin-top: 0.976em;
+ margin-bottom: 0.976em;
+ line-height: 1.067em;
+ font-size: 164%; }
+
+nav.documentation ul {
+ list-style: none; }
+nav.documentation ul ul {
+ margin: 0 0 0 1.5em;
+ list-style: disc; }
+nav.documentation > ul {
+ margin-left: 0; }
+ nav.documentation > ul > li:first-child {
+ font-weight: bold; }
+
+div.feed {
+ margin: 1.6em 0; }
+
+section.author-biography img.avatar {
+ float: left;
+ margin-top: 0.4em;
+ margin-right: 0.8em;
+ border: 1px solid #eedddd;
+ padding: 12px;
+ -webkit-box-shadow: 0px 5px 10px #cecaca;
+ -moz-box-shadow: 0px 5px 10px #cecaca;
+ box-shadow: 0px 5px 10px #cecaca;
+ background: white; }
+section.author-biography div.illustrated p {
+ margin-left: 150px; }
+section.author-biography.footer {
+ border-top: 1px dashed #d7c28b;
+ border-bottom: none;
+ padding-top: 0.8em;
+ padding-bottom: 0; }
+
+article.home section h1 {
+ margin-top: 1.561em;
+ margin-bottom: 0.39em;
+ line-height: 1.067em;
+ font-size: 164%; }
+article.home section.intro heading {
+ display: none; }
+article.home section.intro p {
+ width: 80%;
+ max-width: 23.5em;
+ margin: 0 auto;
+ color: #87877d;
+ margin-top: 1.356em;
+ margin-bottom: 1.356em;
+ line-height: 1.977em;
+ font-size: 118%;
+ text-align: center; }
+article.home section.overview ul, article.home section.features ul {
+ padding-left: 0; }
+article.home section.overview em, article.home section.features em {
+ color: #87877d; }
+article.home section.articles ol li {
+ margin: 0 0 0.875em; }
+ article.home section.articles ol li article h1 {
+ margin-top: 0em;
+ margin-bottom: 0em;
+ line-height: 1.75em;
+ font-size: 100%; }
+ article.home section.articles ol li article p.meta {
+ margin: 0; }
+article.home section.users > ul {
+ margin-left: 0;
+ padding-left: 0;
+ list-style: none; }
+article.home section.users blockquote {
+ padding: 0;
+ margin-top: 0em;
+ margin-bottom: 1.6em;
+ line-height: 1.75em;
+ font-size: 100%; }
+article.home section.users cite {
+ display: block;
+ text-align: right; }
+ article.home section.users cite:before {
+ content: "\2014 "; }
+article.home section.doc-list nav ul li {
+ list-style: none; }
+article.home section.doc-list nav > ul > li:first-child {
+ font-weight: bold; }
+
+@media screen and (min-width: 30em) {
+ header[role=banner] nav.primary ul.menu {
+ padding: 1.6em 0; }
+ header[role=banner] nav.primary ul.menu li {
+ width: 3.069em;
+ margin-right: 2.107em; }
+ header[role=banner] nav.primary ul.menu li:nth-child(even) {
+ float: left; }
+ header[role=banner] nav.primary ul.menu li:last-child {
+ margin-right: 0; } }
+@media screen and (min-width: 40em) {
+ div#content {
+ max-width: 40em; } }
+@media screen and (min-width: 820px) {
+ header[role=banner] hgroup {
+ position: absolute;
+ left: 41.405em;
+ top: 0; }
+ header[role=banner] nav.primary {
+ line-height: 5.25em; }
+ header[role=banner] nav.primary ul li {
+ margin-right: 2.107em; }
+ header[role=banner] p.links {
+ left: 49.405em; }
+
+ div#container {
+ width: 60em; }
+ div#container div#content {
+ width: 34.122em;
+ float: left;
+ padding: 1px 0; }
+ div#container div#content ol, div#container div#content ul {
+ margin-left: 0; }
+ div#container div#sidebar {
+ width: 18.595em;
+ margin-left: 41.405em;
+ padding: 4.8em 0 1.6em; }
+ div#container div#sidebar heading h1 {
+ display: none; }
+ div#container div#sidebar nav.documentation {
+ float: none;
+ width: auto; }
+ div#container div#sidebar nav.documentation ul {
+ float: none;
+ width: auto; }
+ div#container div#sidebar nav.documentation > ul {
+ margin-left: 0; }
+
+ article.home section {
+ clear: both; }
+ article.home section ol, article.home section ul {
+ margin-left: 0; }
+ article.home section.intro p {
+ margin-top: 1.103em;
+ margin-bottom: 1.103em;
+ line-height: 1.609em;
+ font-size: 145%; }
+ article.home section.overview {
+ clear: both;
+ float: left;
+ width: 34.122em; }
+ article.home section.features, article.home section.users {
+ clear: right;
+ float: right;
+ width: 18.595em; }
+ article.home section.getting-started {
+ float: left;
+ width: 18.595em; }
+ article.home section.articles {
+ clear: none;
+ float: left;
+ width: 18.595em;
+ margin-left: 2.107em; }
+
+ p.documentation-overview {
+ float: left;
+ width: 13.32em; }
+
+ nav.documentation {
+ float: right;
+ width: 39.298em; }
+ nav.documentation ul {
+ float: left;
+ width: 18.595em; }
+ nav.documentation ul.quick-start {
+ float: none; }
+ nav.documentation ul.config, nav.documentation ul.deployment, nav.documentation ul.recipes, nav.documentation ul.plugins {
+ margin-left: 2.107em;
+ width: 18.495em; } }
+
+
+div.help {
+ font-size: 12px;
+ color: #999;
+}
+
+/*
+
+Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull
+
+*/
+
+pre code {
+ display: block; padding: 0.5em;
+ background: #fdf6e3; color: #657b83;
+}
+
+pre .comment,
+pre .template_comment,
+pre .diff .header,
+pre .doctype,
+pre .lisp .string,
+pre .javadoc {
+ color: #93a1a1;
+ font-style: italic;
+}
+
+pre .keyword,
+pre .css .rule .keyword,
+pre .winutils,
+pre .javascript .title,
+pre .method,
+pre .addition,
+pre .css .tag,
+pre .lisp .title {
+ color: #859900;
+}
+
+pre .number,
+pre .command,
+pre .string,
+pre .tag .value,
+pre .phpdoc,
+pre .tex .formula,
+pre .regexp,
+pre .hexcolor {
+ color: #2aa198;
+}
+
+pre .title,
+pre .localvars,
+pre .function .title,
+pre .chunk,
+pre .decorator,
+pre .builtin,
+pre .built_in,
+pre .lisp .title,
+pre .identifier,
+pre .title .keymethods,
+pre .id {
+ color: #268bd2;
+}
+
+pre .tag .title,
+pre .rules .property,
+pre .django .tag .keyword {
+ font-weight: bold;
+}
+
+pre .attribute,
+pre .variable,
+pre .instancevar,
+pre .lisp .body,
+pre .smalltalk .number,
+pre .constant,
+pre .class .title,
+pre .parent,
+pre .haskell .label {
+ color: #b58900;
+}
+
+pre .preprocessor,
+pre .pi,
+pre .shebang,
+pre .symbol,
+pre .diff .change,
+pre .special,
+pre .keymethods,
+pre .attr_selector,
+pre .important,
+pre .subst,
+pre .cdata {
+ color: #cb4b16;
+}
+
+pre .deletion {
+ color: #dc322f;
+}
+
+pre .tex .formula {
+ background: #eee8d5;
+}
diff --git a/api/examples/_014_oauth2_client/css/shared.css b/api/examples/_014_oauth2_client/css/shared.css
new file mode 100644
index 0000000..e1e8c10
--- /dev/null
+++ b/api/examples/_014_oauth2_client/css/shared.css
@@ -0,0 +1,39 @@
+.button {
+ color: #333;
+ text-shadow: 0 1px 0 white;
+ border: 1px solid #D4D4D4;
+ border-bottom-color: #BCBCBC;
+ background: #FAFAFA;
+ background: -moz-linear-gradient(#FAFAFA, #EAEAEA);
+ background: -webkit-linear-gradient(#FAFAFA, #EAEAEA);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr='#fafafa', endColorstr='#eaeaea')";
+ position: relative;
+ display: inline-block;
+ padding: 0 10px 0 10px;
+ font-family: Helvetica, arial, freesans, clean, sans-serif;
+ font-size: 13px;
+ font-weight: bold;
+ line-height: 24px;
+ white-space: nowrap;
+ border-radius: 3px;
+ cursor: pointer;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ text-decoration: none;
+}
+
+.button:hover {
+ color: white;
+ text-decoration: none;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3);
+ border-color: #518CC6;
+ border-bottom-color: #2A65A0;
+ background: #599BDC;
+ background: -moz-linear-gradient(#599BDC, #3072B3);
+ background: -webkit-linear-gradient(#599BDC, #3072B3);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr='#599bdc', endColorstr='#3072b3')";
+}
\ No newline at end of file
diff --git a/api/examples/_014_oauth2_client/index.php b/api/examples/_014_oauth2_client/index.php
new file mode 100644
index 0000000..36c98b5
--- /dev/null
+++ b/api/examples/_014_oauth2_client/index.php
@@ -0,0 +1,24 @@
+ Client::class
+]);
+
+(new Restler())->handle();
diff --git a/api/examples/_015_oauth2_server/.htaccess b/api/examples/_015_oauth2_server/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_015_oauth2_server/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_015_oauth2_server/css/lockdin.css b/api/examples/_015_oauth2_server/css/lockdin.css
new file mode 100644
index 0000000..fe9c542
--- /dev/null
+++ b/api/examples/_015_oauth2_server/css/lockdin.css
@@ -0,0 +1,351 @@
+body {
+background: #E9E9E9 url('http://static01.linkedin.com/scds/common/u/img/bg/bg_grain_200x200.png');
+font-family: Arial,Helvetica,"Nimbus Sans L",sans-serif;
+}
+
+a:visited {
+ color: #069;
+}
+a {
+ text-decoration: none;
+ color: #069;
+ outline: none;
+}
+
+#content ul li {
+ font-style:italic;
+ font-size:14px;
+ color:#D85700;
+}
+
+.authorize_options {
+ margin:0;
+ padding:0;
+}
+
+#content .authorize_options li {
+ float:left;
+ list-style-type:none;
+ font-style:normal;
+}
+
+.authorize_options li.cancel { margin-left:15px;padding-top: 8px;}
+
+#container {
+z-index: 10001;
+margin-top: 89px;
+}
+#container {
+margin-left: auto;
+margin-right: auto;
+width: 974px;
+padding: 0;
+}
+
+#content {
+margin-left: auto;
+margin-right: auto;
+width: 974px;
+float: left;
+background:white;
+padding:10px 20px;
+-moz-box-shadow: 0 0 1px #999;
+-webkit-box-shadow: 0 0 1px #999;
+-o-box-shadow: 0 0 1px #999;
+box-shadow: 0 0 1px #999;
+margin-bottom: 10px;
+position: relative;
+}
+
+.global-nav {
+z-index: 10002;
+margin-bottom: 0px;
+left: 0;
+position: fixed;
+top: 0;
+width: 100%;
+}
+
+.global-nav .top-nav {
+height: 38px;
+margin: 0;
+filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FF696969', endColorstr='#FF252525');
+background: #252525;
+background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, dimGray), color-stop(95%, #363636), color-stop(97%, #313131), color-stop(100%, #252525));
+background-image: -webkit-linear-gradient(top, dimGray 0%,#363636 95%,#313131 97%,#252525 100%);
+background-image: -moz-linear-gradient(top, dimGray 0%,#363636 95%,#313131 97%,#252525 100%);
+background-image: -o-linear-gradient(top, dimGray 0%,#363636 95%,#313131 97%,#252525 100%);
+background-image: -ms-linear-gradient(top, dimGray 0%,#363636 95%,#313131 97%,#252525 100%);
+background-image: linear-gradient(top, dimGray 0%,#363636 95%,#313131 97%,#252525 100%);
+}
+
+.global-nav .wrapper {
+margin: 0 auto;
+width: 974px;
+z-index: 10004;
+}
+.primary, .wrapper {
+clear: both;
+}
+.global-nav .util {
+float: right;
+}
+.global-nav ul, .global-nav li, .global-nav p, .global-nav fieldset, #section-header ul, #section-header li, #section-header p, #section-header, .global-search fieldset, .global-search select, .global-search input, #footer ul, #footer li, #footer p {
+margin: 0;
+padding: 0;
+list-style-type: none;
+border: 0;
+outline: 0;
+background: none;
+vertical-align: middle;
+}
+.wrapper .util .tab.username-cont {
+z-index: 10010;
+}
+.global-nav .util .jump {
+position: absolute;
+left: -9999px;
+}
+
+.global-nav .util .tab {
+line-height: 38px;
+}
+.global-nav .tab {
+float: left;
+_border: none;
+_clear: none;
+_padding: 0;
+position: relative;
+z-index: 10010;
+}
+.global-nav p, .global-nav li {
+margin-bottom: 0;
+}
+
+.global-nav ul, .global-nav li, .global-nav p, .global-nav fieldset, #section-header ul, #section-header li, #section-header p, #section-header, .global-search fieldset, .global-search select, .global-search input, #footer ul, #footer li, #footer p {
+margin: 0;
+padding: 0;
+list-style-type: none;
+border: 0;
+outline: 0;
+background: none;
+vertical-align: middle;
+}
+
+.global-nav .util .username {
+color: white;
+}
+.global-nav .tab .tab-name {
+color: #71C5EF;
+display: block;
+padding: 0 10px;
+}
+.global-nav .util .username .menu-indicator {
+margin: 0 0 0 4px;
+display: -moz-inline-box;
+-moz-box-orient: vertical;
+display: inline-block;
+vertical-align: middle;
+height: 0;
+width: 0;
+_font-size: 0;
+_line-height: 0;
+border-style: dashed;
+border-color: transparent;
+border-width: 4px 4px 0;
+border-top-color: white;
+border-top-style: solid;
+}
+.global-nav .tab .tab-name span {
+position: relative;
+z-index: 10011;
+}
+.global-nav .logo {
+float: left;
+margin: 8px 10px 0 3px;
+}
+.global-nav .logo a {
+ color:white;
+}
+.global-nav .logo a span{
+ background-color:#069;
+ padding:0px 3px;
+}
+
+.global-nav .account {
+float: left;
+font-size: 12px;
+margin-top: 14px;
+}
+
+.global-nav #header-notifications.v2 {
+background: url('http://static01.linkedin.com/scds/common/u/img/anim/anim_loading_16x16.gif') no-repeat -12345px -12345px;
+}
+.global-nav .header-notifications {
+border: none;
+float: left;
+margin-left: 15px;
+padding: 0;
+}
+.global-nav .bottom-nav {
+height: 39px;
+margin: 0;
+filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FF121212', endColorstr='#FF303030');
+background: #303030;
+background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #121212), color-stop(100%, #303030));
+background-image: -webkit-linear-gradient(top, #121212 0%,#303030 100%);
+background-image: -moz-linear-gradient(top, #121212 0%,#303030 100%);
+background-image: -o-linear-gradient(top, #121212 0%,#303030 100%);
+background-image: -ms-linear-gradient(top, #121212 0%,#303030 100%);
+background-image: linear-gradient(top, #121212 0%,#303030 100%);
+}
+
+.global-nav .global-search {
+float: right;
+margin: 6px 0 0;
+padding: 0;
+}
+
+.global-nav .global-search fieldset {
+float: left;
+white-space: nowrap;
+}
+
+.global-nav .global-search legend, .global-nav .global-search legend span, .global-nav .global-search label {
+left: -12345px;
+position: absolute;
+}
+
+.global-nav .global-search .search-scope {
+font-size: 12px;
+float: left;
+line-height: 13px;
+z-index: 10008;
+}
+.global-nav .global-search .search-scope .label span {
+background: transparent url('http://static01.linkedin.com/scds/common/u/img/sprite/sprite_global_v8.png') no-repeat 100% -1914px;
+color: white;
+cursor: pointer;
+float: left;
+padding: 6px 15px 6px 11px;
+margin-right: 10px;
+}
+ .global-nav .global-search .search-scope .label {
+border: 1px solid #1A2229;
+float: left;
+left: 0;
+position: static;
+margin: 0;
+_vertical-align: -5px;
+-moz-border-radius-topleft: 4px;
+-webkit-border-top-left-radius: 4px;
+-o-border-top-left-radius: 4px;
+-ms-border-top-left-radius: 4px;
+-khtml-border-top-left-radius: 4px;
+border-top-left-radius: 4px;
+-moz-border-radius-bottomleft: 4px;
+-webkit-border-bottom-left-radius: 4px;
+-o-border-bottom-left-radius: 4px;
+-ms-border-bottom-left-radius: 4px;
+-khtml-border-bottom-left-radius: 4px;
+border-bottom-left-radius: 4px;
+filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FF696969', endColorstr='#FF252525');
+background: #252525;
+background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, dimGray), color-stop(95%, #363636), color-stop(97%, #313131), color-stop(100%, #252525));
+background-image: -webkit-linear-gradient(top, dimGray 0%,#363636 95%,#313131 97%,#252525 100%);
+background-image: -moz-linear-gradient(top, dimGray 0%,#363636 95%,#313131 97%,#252525 100%);
+background-image: -o-linear-gradient(top, dimGray 0%,#363636 95%,#313131 97%,#252525 100%);
+background-image: -ms-linear-gradient(top, dimGray 0%,#363636 95%,#313131 97%,#252525 100%);
+background-image: linear-gradient(top, dimGray 0%,#363636 95%,#313131 97%,#252525 100%);
+}
+.global-nav .global-search .search-box-container {
+background: white;
+border: 1px solid #1A2229;
+border-left: none;
+display: block;
+float: left;
+padding: 0;
+-moz-border-radius-topright: 4px 4px;
+-webkit-border-top-right-radius: 4px 4px;
+-o-border-top-right-radius: 4px 4px;
+-ms-border-top-right-radius: 4px 4px;
+-khtml-border-top-right-radius: 4px 4px;
+border-top-right-radius: 4px 4px;
+-moz-border-radius-bottomright: 4px 4px;
+-webkit-border-bottom-right-radius: 4px 4px;
+-o-border-bottom-right-radius: 4px 4px;
+-ms-border-bottom-right-radius: 4px 4px;
+-khtml-border-bottom-right-radius: 4px 4px;
+border-bottom-right-radius: 4px 4px;
+}
+.global-nav .global-search .search-box-container .search-term {
+border: none;
+font-size: 13px;
+line-height: 13px;
+margin: 0;
+padding: 5px;
+position: relative;
+width: 205px;
+z-index: 3;
+}
+#search-autocomplete-container {
+position: relative;
+display: inline;
+float: left;
+padding-right: 3px;
+_padding-right: 0;
+_float: none;
+}
+.global-nav .global-search .search-box-container .search-go {
+background: transparent url('http://static01.linkedin.com/scds/common/u/img/sprite/sprite_global_v8.png') no-repeat 0 -2197px;
+border: none;
+color: #0076A8;
+cursor: pointer;
+height: 20px;
+line-height: 1em;
+margin: 3px 5px 0 0;
+padding: 0px;
+width: 18px;
+text-indent: -1234px;
+_width: 22px;
+}
+
+.global-nav .global-search .search-link {
+color: white;
+float: left;
+font-size: 11px;
+line-height: 29px;
+margin: 0 0 0 12px;
+}
+
+.global-nav .nav .tab {
+ line-height: 37px;
+}
+
+.global-nav .tab:hover .tab-name, .global-nav .tab.hover .tab-name, .global-nav .tab.selected .tab-name {
+color: white;
+}
+
+.global-nav .tab .tab-name {
+color: #71C5EF;
+display: block;
+padding: 0 10px;
+text-decoration: none;
+}
+
+.global-nav {
+font-family: sans-serif;
+}
+.global-nav {
+font-size: 13px;
+}
+
+.global-nav .account {
+float: left;
+font-size: 12px;
+margin-top: 14px;
+}
+
+.global-nav .account span {
+color: #87877D;
+}
\ No newline at end of file
diff --git a/api/examples/_015_oauth2_server/css/shared.css b/api/examples/_015_oauth2_server/css/shared.css
new file mode 100644
index 0000000..e1e8c10
--- /dev/null
+++ b/api/examples/_015_oauth2_server/css/shared.css
@@ -0,0 +1,39 @@
+.button {
+ color: #333;
+ text-shadow: 0 1px 0 white;
+ border: 1px solid #D4D4D4;
+ border-bottom-color: #BCBCBC;
+ background: #FAFAFA;
+ background: -moz-linear-gradient(#FAFAFA, #EAEAEA);
+ background: -webkit-linear-gradient(#FAFAFA, #EAEAEA);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr='#fafafa', endColorstr='#eaeaea')";
+ position: relative;
+ display: inline-block;
+ padding: 0 10px 0 10px;
+ font-family: Helvetica, arial, freesans, clean, sans-serif;
+ font-size: 13px;
+ font-weight: bold;
+ line-height: 24px;
+ white-space: nowrap;
+ border-radius: 3px;
+ cursor: pointer;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ text-decoration: none;
+}
+
+.button:hover {
+ color: white;
+ text-decoration: none;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3);
+ border-color: #518CC6;
+ border-bottom-color: #2A65A0;
+ background: #599BDC;
+ background: -moz-linear-gradient(#599BDC, #3072B3);
+ background: -webkit-linear-gradient(#599BDC, #3072B3);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr='#599bdc', endColorstr='#3072b3')";
+}
\ No newline at end of file
diff --git a/api/examples/_015_oauth2_server/index.php b/api/examples/_015_oauth2_server/index.php
new file mode 100644
index 0000000..30c4796
--- /dev/null
+++ b/api/examples/_015_oauth2_server/index.php
@@ -0,0 +1,26 @@
+ Server::class
+]);
+
+(new Restler())->handle();
diff --git a/api/examples/_016_forms/.htaccess b/api/examples/_016_forms/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_016_forms/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_016_forms/css/glyphicons.css b/api/examples/_016_forms/css/glyphicons.css
new file mode 100644
index 0000000..a800593
--- /dev/null
+++ b/api/examples/_016_forms/css/glyphicons.css
@@ -0,0 +1,217 @@
+@font-face {
+ font-family: 'Glyphicons Halflings';
+ src: url('//netdna.bootstrapcdn.com/bootstrap/3.0.0/fonts/glyphicons-halflings-regular.eot');
+ src: url('//netdna.bootstrapcdn.com/bootstrap/3.0.0/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('//netdna.bootstrapcdn.com/bootstrap/3.0.0/fonts/glyphicons-halflings-regular.woff') format('woff'), url('//netdna.bootstrapcdn.com/bootstrap/3.0.0/fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('//netdna.bootstrapcdn.com/bootstrap/3.0.0/fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg');
+}
+
+.glyphicon {
+ position: relative;
+ top: 1px;
+ display: inline-block;
+ font-family: 'Glyphicons Halflings';
+ font-style: normal;
+ font-weight: normal;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+}
+
+.glyphicon-asterisk:before{content:"\2a";}
+.glyphicon-plus:before{content:"\2b";}
+.glyphicon-euro:before{content:"\20ac";}
+.glyphicon-minus:before{content:"\2212";}
+.glyphicon-cloud:before{content:"\2601";}
+.glyphicon-envelope:before{content:"\2709";}
+.glyphicon-pencil:before{content:"\270f";}
+.glyphicon-glass:before{content:"\e001";}
+.glyphicon-music:before{content:"\e002";}
+.glyphicon-search:before{content:"\e003";}
+.glyphicon-heart:before{content:"\e005";}
+.glyphicon-star:before{content:"\e006";}
+.glyphicon-star-empty:before{content:"\e007";}
+.glyphicon-user:before{content:"\e008";}
+.glyphicon-film:before{content:"\e009";}
+.glyphicon-th-large:before{content:"\e010";}
+.glyphicon-th:before{content:"\e011";}
+.glyphicon-th-list:before{content:"\e012";}
+.glyphicon-ok:before{content:"\e013";}
+.glyphicon-remove:before{content:"\e014";}
+.glyphicon-zoom-in:before{content:"\e015";}
+.glyphicon-zoom-out:before{content:"\e016";}
+.glyphicon-off:before{content:"\e017";}
+.glyphicon-signal:before{content:"\e018";}
+.glyphicon-cog:before{content:"\e019";}
+.glyphicon-trash:before{content:"\e020";}
+.glyphicon-home:before{content:"\e021";}
+.glyphicon-file:before{content:"\e022";}
+.glyphicon-time:before{content:"\e023";}
+.glyphicon-road:before{content:"\e024";}
+.glyphicon-download-alt:before{content:"\e025";}
+.glyphicon-download:before{content:"\e026";}
+.glyphicon-upload:before{content:"\e027";}
+.glyphicon-inbox:before{content:"\e028";}
+.glyphicon-play-circle:before{content:"\e029";}
+.glyphicon-repeat:before{content:"\e030";}
+.glyphicon-refresh:before{content:"\e031";}
+.glyphicon-list-alt:before{content:"\e032";}
+.glyphicon-flag:before{content:"\e034";}
+.glyphicon-headphones:before{content:"\e035";}
+.glyphicon-volume-off:before{content:"\e036";}
+.glyphicon-volume-down:before{content:"\e037";}
+.glyphicon-volume-up:before{content:"\e038";}
+.glyphicon-qrcode:before{content:"\e039";}
+.glyphicon-barcode:before{content:"\e040";}
+.glyphicon-tag:before{content:"\e041";}
+.glyphicon-tags:before{content:"\e042";}
+.glyphicon-book:before{content:"\e043";}
+.glyphicon-print:before{content:"\e045";}
+.glyphicon-font:before{content:"\e047";}
+.glyphicon-bold:before{content:"\e048";}
+.glyphicon-italic:before{content:"\e049";}
+.glyphicon-text-height:before{content:"\e050";}
+.glyphicon-text-width:before{content:"\e051";}
+.glyphicon-align-left:before{content:"\e052";}
+.glyphicon-align-center:before{content:"\e053";}
+.glyphicon-align-right:before{content:"\e054";}
+.glyphicon-align-justify:before{content:"\e055";}
+.glyphicon-list:before{content:"\e056";}
+.glyphicon-indent-left:before{content:"\e057";}
+.glyphicon-indent-right:before{content:"\e058";}
+.glyphicon-facetime-video:before{content:"\e059";}
+.glyphicon-picture:before{content:"\e060";}
+.glyphicon-map-marker:before{content:"\e062";}
+.glyphicon-adjust:before{content:"\e063";}
+.glyphicon-tint:before{content:"\e064";}
+.glyphicon-edit:before{content:"\e065";}
+.glyphicon-share:before{content:"\e066";}
+.glyphicon-check:before{content:"\e067";}
+.glyphicon-move:before{content:"\e068";}
+.glyphicon-step-backward:before{content:"\e069";}
+.glyphicon-fast-backward:before{content:"\e070";}
+.glyphicon-backward:before{content:"\e071";}
+.glyphicon-play:before{content:"\e072";}
+.glyphicon-pause:before{content:"\e073";}
+.glyphicon-stop:before{content:"\e074";}
+.glyphicon-forward:before{content:"\e075";}
+.glyphicon-fast-forward:before{content:"\e076";}
+.glyphicon-step-forward:before{content:"\e077";}
+.glyphicon-eject:before{content:"\e078";}
+.glyphicon-chevron-left:before{content:"\e079";}
+.glyphicon-chevron-right:before{content:"\e080";}
+.glyphicon-plus-sign:before{content:"\e081";}
+.glyphicon-minus-sign:before{content:"\e082";}
+.glyphicon-remove-sign:before{content:"\e083";}
+.glyphicon-ok-sign:before{content:"\e084";}
+.glyphicon-question-sign:before{content:"\e085";}
+.glyphicon-info-sign:before{content:"\e086";}
+.glyphicon-screenshot:before{content:"\e087";}
+.glyphicon-remove-circle:before{content:"\e088";}
+.glyphicon-ok-circle:before{content:"\e089";}
+.glyphicon-ban-circle:before{content:"\e090";}
+.glyphicon-arrow-left:before{content:"\e091";}
+.glyphicon-arrow-right:before{content:"\e092";}
+.glyphicon-arrow-up:before{content:"\e093";}
+.glyphicon-arrow-down:before{content:"\e094";}
+.glyphicon-share-alt:before{content:"\e095";}
+.glyphicon-resize-full:before{content:"\e096";}
+.glyphicon-resize-small:before{content:"\e097";}
+.glyphicon-exclamation-sign:before{content:"\e101";}
+.glyphicon-gift:before{content:"\e102";}
+.glyphicon-leaf:before{content:"\e103";}
+.glyphicon-eye-open:before{content:"\e105";}
+.glyphicon-eye-close:before{content:"\e106";}
+.glyphicon-warning-sign:before{content:"\e107";}
+.glyphicon-plane:before{content:"\e108";}
+.glyphicon-random:before{content:"\e110";}
+.glyphicon-comment:before{content:"\e111";}
+.glyphicon-magnet:before{content:"\e112";}
+.glyphicon-chevron-up:before{content:"\e113";}
+.glyphicon-chevron-down:before{content:"\e114";}
+.glyphicon-retweet:before{content:"\e115";}
+.glyphicon-shopping-cart:before{content:"\e116";}
+.glyphicon-folder-close:before{content:"\e117";}
+.glyphicon-folder-open:before{content:"\e118";}
+.glyphicon-resize-vertical:before{content:"\e119";}
+.glyphicon-resize-horizontal:before{content:"\e120";}
+.glyphicon-hdd:before{content:"\e121";}
+.glyphicon-bullhorn:before{content:"\e122";}
+.glyphicon-certificate:before{content:"\e124";}
+.glyphicon-thumbs-up:before{content:"\e125";}
+.glyphicon-thumbs-down:before{content:"\e126";}
+.glyphicon-hand-right:before{content:"\e127";}
+.glyphicon-hand-left:before{content:"\e128";}
+.glyphicon-hand-up:before{content:"\e129";}
+.glyphicon-hand-down:before{content:"\e130";}
+.glyphicon-circle-arrow-right:before{content:"\e131";}
+.glyphicon-circle-arrow-left:before{content:"\e132";}
+.glyphicon-circle-arrow-up:before{content:"\e133";}
+.glyphicon-circle-arrow-down:before{content:"\e134";}
+.glyphicon-globe:before{content:"\e135";}
+.glyphicon-tasks:before{content:"\e137";}
+.glyphicon-filter:before{content:"\e138";}
+.glyphicon-fullscreen:before{content:"\e140";}
+.glyphicon-dashboard:before{content:"\e141";}
+.glyphicon-heart-empty:before{content:"\e143";}
+.glyphicon-link:before{content:"\e144";}
+.glyphicon-phone:before{content:"\e145";}
+.glyphicon-usd:before{content:"\e148";}
+.glyphicon-gbp:before{content:"\e149";}
+.glyphicon-sort:before{content:"\e150";}
+.glyphicon-sort-by-alphabet:before{content:"\e151";}
+.glyphicon-sort-by-alphabet-alt:before{content:"\e152";}
+.glyphicon-sort-by-order:before{content:"\e153";}
+.glyphicon-sort-by-order-alt:before{content:"\e154";}
+.glyphicon-sort-by-attributes:before{content:"\e155";}
+.glyphicon-sort-by-attributes-alt:before{content:"\e156";}
+.glyphicon-unchecked:before{content:"\e157";}
+.glyphicon-expand:before{content:"\e158";}
+.glyphicon-collapse-down:before{content:"\e159";}
+.glyphicon-collapse-up:before{content:"\e160";}
+.glyphicon-log-in:before{content:"\e161";}
+.glyphicon-flash:before{content:"\e162";}
+.glyphicon-log-out:before{content:"\e163";}
+.glyphicon-new-window:before{content:"\e164";}
+.glyphicon-record:before{content:"\e165";}
+.glyphicon-save:before{content:"\e166";}
+.glyphicon-open:before{content:"\e167";}
+.glyphicon-saved:before{content:"\e168";}
+.glyphicon-import:before{content:"\e169";}
+.glyphicon-export:before{content:"\e170";}
+.glyphicon-send:before{content:"\e171";}
+.glyphicon-floppy-disk:before{content:"\e172";}
+.glyphicon-floppy-saved:before{content:"\e173";}
+.glyphicon-floppy-remove:before{content:"\e174";}
+.glyphicon-floppy-save:before{content:"\e175";}
+.glyphicon-floppy-open:before{content:"\e176";}
+.glyphicon-credit-card:before{content:"\e177";}
+.glyphicon-transfer:before{content:"\e178";}
+.glyphicon-cutlery:before{content:"\e179";}
+.glyphicon-header:before{content:"\e180";}
+.glyphicon-compressed:before{content:"\e181";}
+.glyphicon-earphone:before{content:"\e182";}
+.glyphicon-phone-alt:before{content:"\e183";}
+.glyphicon-tower:before{content:"\e184";}
+.glyphicon-stats:before{content:"\e185";}
+.glyphicon-sd-video:before{content:"\e186";}
+.glyphicon-hd-video:before{content:"\e187";}
+.glyphicon-subtitles:before{content:"\e188";}
+.glyphicon-sound-stereo:before{content:"\e189";}
+.glyphicon-sound-dolby:before{content:"\e190";}
+.glyphicon-sound-5-1:before{content:"\e191";}
+.glyphicon-sound-6-1:before{content:"\e192";}
+.glyphicon-sound-7-1:before{content:"\e193";}
+.glyphicon-copyright-mark:before{content:"\e194";}
+.glyphicon-registration-mark:before{content:"\e195";}
+.glyphicon-cloud-download:before{content:"\e197";}
+.glyphicon-cloud-upload:before{content:"\e198";}
+.glyphicon-tree-conifer:before{content:"\e199";}
+.glyphicon-tree-deciduous:before{content:"\e200";}
+.glyphicon-briefcase:before{content:"\1f4bc";}
+.glyphicon-calendar:before{content:"\1f4c5";}
+.glyphicon-pushpin:before{content:"\1f4cc";}
+.glyphicon-paperclip:before{content:"\1f4ce";}
+.glyphicon-camera:before{content:"\1f4f7";}
+.glyphicon-lock:before{content:"\1f512";}
+.glyphicon-bell:before{content:"\1f514";}
+.glyphicon-bookmark:before{content:"\1f516";}
+.glyphicon-fire:before{content:"\1f525";}
+.glyphicon-wrench:before{content:"\1f527";}
\ No newline at end of file
diff --git a/api/examples/_016_forms/index.php b/api/examples/_016_forms/index.php
index b3bc12a..9ecec00 100644
--- a/api/examples/_016_forms/index.php
+++ b/api/examples/_016_forms/index.php
@@ -1,106 +1,25 @@
= 5.3
- Description:
- Forms is a utility class that builds web forms in one of the following in built styles
-
- - Plain Html 5
- - Twitter Bootstrap 3
- - Zerb Foundation 5
-
-```php
-use Luracast/Restler/UI/Forms;
-use Luracast/Restler/UI/FormStyles;
-
-Forms::$style = FormStyles::$bootstrap3; // FormStyles::$foundation5;
-```
-
-Where ever you need the generated forms (typically in view templates) just call
-
-```php
-echo Forms::get('POST','user/signup');
-```
-
-If you are using twig templates you can use the following instead
-
- {{ form('POST', 'user/signup') }}
-
-## Emmet Templates
-
-Forms is using Emmet templates, with which adding more styles is very easy
-
-Emmet Templates is built with a subset of Emmet as in [emmet.io](http://emmet.io/) extended to serve as a
-template engine
-
-For example
-
- .form-group>label{$label#}+input.form-control[value=$value# type=$type#
-
-Expands to the following html
-
-```html
-
-
-
-
-```
-
-When the given data is
-```php
-array(
- 'label' => 'Email',
- 'value' => 'arul@luracast.com',
- 'type' => 'email'
-);
-```
-Typically this data is supplied by the metadata extracted from the php-doc comments of of the api parameters
-
-Users.php and Address.php shows the bare minimum code needed to get create forms.
-
-Check out the resulting form [here](users.html).
-[![Forms](../resources/Form.gif)](users.html)
-We have made it easy to try different styles
-Also this example serves as an example for our Blade template integration
-
-See bootstrap3.blade.php and foundation5.blade.php
-
- Content:
-
-*[bootstrap3.blade.php]: _016_forms/views/base/bootstrap3.blade.php
-*[foundation5.blade.php]: _016_forms/views/base/foundation5.blade.php
- */
-$loader = include '../../../vendor/autoload.php';
-$loader->setUseIncludePath(true);
-
-use Luracast\Restler\Data\Validator;
-use Luracast\Restler\Restler;
use Luracast\Restler\Defaults;
-use Luracast\Restler\Format\HtmlFormat;
-use Luracast\Restler\UI\Forms;
-use Luracast\Restler\UI\FormStyles;
+use Luracast\Restler\MediaTypes\Html;
+use Luracast\Restler\MediaTypes\Json;
+use Luracast\Restler\Middleware\SessionMiddleware;
+use Luracast\Restler\Restler;
+use Luracast\Restler\Router;
-HtmlFormat::$viewPath = __DIR__ . '/views';
-HtmlFormat::$template = 'blade';
-Validator::$holdException = true;
+define('BASE', __DIR__ . '/../../..');
+include BASE . "/vendor/autoload.php";
-$themes = array(
- 'amelia', 'cerulean', 'cosmo',
- 'cyborg', 'darkly', 'flatly',
- 'journal', 'lumen', 'readable',
- 'simplex', 'slate', 'spacelab',
- 'superhero', 'united', 'yeti',
+Defaults::$cacheDirectory = BASE . '/api/common/store';
+Html::$template = 'blade'; //'handlebar'; //'twig'; //'php';
+Restler::$middleware[] = new SessionMiddleware();
+Router::setOverridingResponseMediaTypes(
+ Json::class,
+ Html::class
);
-$theme = isset($_GET['theme']) ? $_GET['theme'] : $themes[array_rand($themes, 1)];
-$style = $theme == 'foundation5' ? 'foundation5' : 'bootstrap3';
-HtmlFormat::$data += compact('theme', 'themes', 'style');
-
-Forms::$style = FormStyles::$$style;
+Router::mapApiClasses([
+ Users::class
+]);
-$r = new Restler();
-$r->setSupportedFormats('HtmlFormat');
-$r->addAPIClass('Users');
-$r->handle();
\ No newline at end of file
+(new Restler())->handle();
diff --git a/api/examples/_017_graphql/.htaccess b/api/examples/_017_graphql/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/examples/_017_graphql/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/examples/_017_graphql/index.php b/api/examples/_017_graphql/index.php
new file mode 100644
index 0000000..e821273
--- /dev/null
+++ b/api/examples/_017_graphql/index.php
@@ -0,0 +1,8 @@
+handle();
diff --git a/api/examples/_017_graphql/routes.php b/api/examples/_017_graphql/routes.php
new file mode 100644
index 0000000..3ba0f37
--- /dev/null
+++ b/api/examples/_017_graphql/routes.php
@@ -0,0 +1,57 @@
+ GraphQLType::int(),
+ 'args' => [
+ 'x' => ['type' => GraphQLType::int(), 'defaultValue' => 5],
+ 'y' => GraphQLType::nonNull(GraphQL::enum([
+ 'name' => 'SelectedEnum',
+ 'description' => 'selected range of numbers',
+ 'values' => ['five' => 5, 'seven' => 7, 'nine' => 9]
+ ])),
+ ],
+ 'resolve' => function ($root, $args) {
+ return $args['x'] + $args['y'];
+ },
+];
+GraphQL::$mutations['sumUp'] = [
+ 'type' => GraphQLType::int(),
+ 'args' => [
+ 'numbers' => GraphQLType::listOf(GraphQLType::int())
+ ],
+ 'resolve' => function ($root, $args) {
+ return array_sum($args['numbers']);
+ },
+];
+GraphQL::$queries['echo'] = [
+ 'type' => GraphQLType::string(),
+ 'args' => [
+ 'message' => ['type' => GraphQLType::nonNull(GraphQLType::string()), 'defaultValue' => 'Hello'],
+ ],
+ 'resolve' => function ($root, $args) {
+ return $root['prefix'] . $args['message'];
+ }
+];
+
+GraphQL::mapApiClasses([
+ RateLimitedAuthors::class,
+ Say::class,
+ BMI2::class,
+ Access::class,
+ Tasks::class,
+]);
+GraphQL::addMethod(new ReflectionMethod(Math::class, 'add'));
+GraphQL::addMethod(new ReflectionMethod(Math::class, 'sum2'));
+GraphQL::addMethod(new ReflectionMethod(Type::class, 'postEnumerator'));
diff --git a/api/tests/.htaccess b/api/tests/.htaccess
new file mode 100644
index 0000000..0704f69
--- /dev/null
+++ b/api/tests/.htaccess
@@ -0,0 +1,11 @@
+DirectoryIndex index.php
+
+ RewriteEngine On
+ RewriteRule ^$ index.php [QSA,L]
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+ php_flag display_errors Off
+
\ No newline at end of file
diff --git a/api/tests/index.php b/api/tests/index.php
new file mode 100644
index 0000000..415c854
--- /dev/null
+++ b/api/tests/index.php
@@ -0,0 +1,28 @@
+ MinMax::class,
+ 'param/minmaxfix' => MinMaxFix::class,
+ 'param/type' => Type::class,
+ 'param/validation' => Validation::class,
+ 'request_data' => Data::class,
+ 'upload/files' => Files::class,
+ 'storage/cache' => CacheTest::class,
+ 'storage/session' => SessionTest::class,
+]);
+
+(new Restler())->handle();
diff --git a/api/tests/param/Type.php b/api/tests/param/Type.php
index 3cfd713..8a1d26b 100644
--- a/api/tests/param/Type.php
+++ b/api/tests/param/Type.php
@@ -2,6 +2,14 @@
class Type
{
+ /**
+ * @param int $selected {@select one,two,three}{@choice 1,2,3}
+ * @return int
+ */
+ function postEnumerator(int $selected)
+ {
+ return $selected;
+ }
/**
* UUID validation
diff --git a/behat.yml b/behat.yml
index c6e93ca..e739eec 100644
--- a/behat.yml
+++ b/behat.yml
@@ -2,7 +2,7 @@
default:
gherkin:
filters:
- tags: "~@cookie&&~@session"
+ tags: ~@fpm&&~@cookie&&~@session
suites:
default:
contexts:
@@ -11,6 +11,8 @@ default:
fpm:
suites:
default:
+ filters:
+ tags: ~@default&&~@cookie&&~@session
contexts:
- RestContext:
- - http://localhost/restler4/
+ - http://localhost
diff --git a/composer.json b/composer.json
index 2c0f790..7ffd193 100644
--- a/composer.json
+++ b/composer.json
@@ -3,7 +3,7 @@
"description": "Running Restler on ReactPHP",
"type": "project",
"require": {
- "php": "^7.1",
+ "php": "^7.1|^8",
"ext-json": "*",
"psr/container": "^1.0",
"guzzlehttp/guzzle": "~7",
@@ -18,18 +18,21 @@
"behat/behat": "~3",
"psy/psysh": "@stable",
"react/event-loop": "@stable",
- "react/http": "@stable",
+ "react/http": "^1.1",
"workerman/workerman": "^4.0",
"twig/twig": "^3",
"mustache/mustache": "^2",
"illuminate/view": "^8 || ^7",
"bshaffer/oauth2-server-php": "^1.11",
- "react/http-client": "^0.5.9",
"swoole/ide-helper": "^4.3",
"box/spout": "^3.1",
"bref/bref": "^0.5.20",
"rize/uri-template": "^0.3.2",
- "webonyx/graphql-php": "^14.1"
+ "webonyx/graphql-php": "^14.1",
+ "gabordemooij/redbean": "^5.5",
+ "rector/rector-prefixed": "^0.8.23",
+ "symplify/easy-coding-standard": "^8.3",
+ "firebase/php-jwt": "^5.2"
},
"license": "MIT",
"authors": [
@@ -49,12 +52,12 @@
},
"autoload-dev": {
"classmap": [
- "api/tests/",
"api/tests/param/",
"api/tests/request_data/",
"api/tests/upload/",
"api/tests/storage/",
"api/common/",
+ "api/examples/-storage-/",
"api/examples/_001_helloworld/",
"api/examples/_002_minimal/",
"api/examples/_003_multiformat/",
@@ -73,7 +76,8 @@
"api/examples/_013_html",
"api/examples/_014_oauth2_client",
"api/examples/_015_oauth2_server",
- "api/examples/_016_forms"
+ "api/examples/_016_forms",
+ "api/examples/_017_graphql"
]
},
"config": {
diff --git a/features/bootstrap/RestContext.php b/features/bootstrap/RestContext.php
index 5b88a8c..8bac24f 100644
--- a/features/bootstrap/RestContext.php
+++ b/features/bootstrap/RestContext.php
@@ -14,6 +14,7 @@
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Rize\UriTemplate\UriTemplate;
+use Symfony\Component\Console\Input\ArgvInput;
/**
* Rest context.
@@ -29,7 +30,7 @@
class RestContext implements Context
{
public const COOKIE_FILE = 'behat-guzzle-cookie-data.json';
-
+ protected $baseUrl = '';
private $_request_debug_stream = null;
private $_startTime = null;
private $_restObject = null;
@@ -51,43 +52,6 @@ class RestContext implements Context
private $_language = null;
private $_data = null;
- protected $baseUrl = '';
-
-
- /**
- * @BeforeSuite
- *
- * @param BeforeSuiteScope $scope
- */
- public static function prepare(BeforeSuiteScope $scope)
- {
- $environment = $scope->getEnvironment();
- $contexts = $environment->getContextClassesWithArguments();
- $baseUrl = $contexts[static::class][0];
- // prepare system for test suite
- // before it runs
- $client = new Client(['base_uri' => $baseUrl]);
- $result = $client->delete('-storage-');
- }
-
- /**
- * @AfterSuite
- *
- * @param AfterSuiteScope $scope
- */
- public static function package(AfterSuiteScope $scope)
- {
- $environment = $scope->getEnvironment();
- $contexts = $environment->getContextClassesWithArguments();
- $baseUrl = $contexts[static::class][0];
- // package all loaded files in cache folder
- // after all tests completed
- $client = new Client(['base_uri' => $baseUrl]);
- //load explorer dependencies
- $client->get('explorer/docs.json');
- $result = $client->get('-storage-/pack');
- }
-
/**
* Initializes context.
* Every scenario gets it's own context object.
@@ -139,6 +103,54 @@ function (\Guzzle\Common\Event $event) {
}
}
+ /**
+ * @BeforeSuite
+ *
+ * @param NotBeforeSuiteScope $scope
+ */
+ public static function prepare(BeforeSuiteScope $scope)
+ {
+ $environment = $scope->getEnvironment();
+ $contexts = $environment->getContextClassesWithArguments();
+ $baseUrl = $contexts[static::class][0];
+ // prepare system for test suite
+ // before it runs
+ $client = new Client(['base_uri' => $baseUrl]);
+ $result = $client->delete('examples/-storage-/all');
+ }
+
+ /**
+ * @AfterSuite
+ *
+ * @param NotAfterSuiteScope $scope
+ */
+ public static function package(AfterSuiteScope $scope)
+ {
+ $input = new ArgvInput($_SERVER['argv']);
+ $profile = $input->getParameterOption(['--profile', '-p']) ?: 'default';
+ if ('default' !== $profile) {
+ return;
+ }
+ $environment = $scope->getEnvironment();
+ $contexts = $environment->getContextClassesWithArguments();
+ $baseUrl = $contexts[static::class][0];
+ // package all loaded files in cache folder
+ // after all tests completed
+ $client = new Client(['base_uri' => $baseUrl]);
+ //load explorer dependencies
+ $client->get('explorer/docs.json');
+ $result = $client->get('examples/-storage-/pack');
+ }
+
+ /**
+ * @Given /^that I send:/
+ * @param PyStringNode $data
+ */
+ public function thatISendPyString(PyStringNode $data)
+ {
+ $this->thatISend($data);
+ }
+
/**
* ============ json array ===================
* @Given /^that I send (\[[^]]*\])$/
@@ -161,15 +173,6 @@ public function thatISend($data)
$this->_restObjectMethod = 'post';
}
- /**
- * @Given /^that I send:/
- * @param PyStringNode $data
- */
- public function thatISendPyString(PyStringNode $data)
- {
- $this->thatISend($data);
- }
-
/**
* ============ json array ===================
* @Given /^the response contains (\[[^]]*\])$/
@@ -197,6 +200,54 @@ public function theResponseContains($response)
}
}
+ /**
+ * @Then /^echo last response$/
+ */
+ public function echoLastResponse()
+ {
+ global $argv;
+ $level = 1;
+ if (in_array('-v', $argv) || in_array('--verbose=1', $argv)) {
+ $level = 2;
+ } elseif (in_array('-vv', $argv) || in_array('--verbose=2', $argv)) {
+ $level = 3;
+ } elseif (in_array('-vvv', $argv) || in_array('--verbose=3', $argv)) {
+ $level = 4;
+ }
+ //echo "$this->_request\n$this->_response";
+ if ($level >= 2 && is_resource($this->_request_debug_stream)) {
+ rewind($this->_request_debug_stream);
+ echo stream_get_contents($this->_request_debug_stream) . PHP_EOL . PHP_EOL;
+ }
+ if ($level >= 1) {
+ /** @var RequestInterface $req */
+ $req = $this->_request;
+ echo $req->getMethod() . ' ' . $req->getUri() . ' HTTP/' . $req->getProtocolVersion() . PHP_EOL;
+ foreach ($req->getHeaders() as $k => $v) {
+ echo ucwords($k) . ': ' . implode(', ', $v) . PHP_EOL;
+ }
+ echo PHP_EOL;
+ echo urldecode((string)$req->getBody()) . PHP_EOL . PHP_EOL;
+ }
+ /** @var ResponseInterface $res */
+ $res = $this->_response;
+ echo 'HTTP/' . $res->getProtocolVersion() . ' ' . $res->getStatusCode() . ' ' . $res->getReasonPhrase() . PHP_EOL;
+ foreach ($res->getHeaders() as $k => $v) {
+ echo ucwords($k) . ': ' . implode(', ', $v) . PHP_EOL;
+ }
+ echo PHP_EOL;
+ echo (string)$res->getBody();
+ }
+
+ /**
+ * @Given /^the response equals:/
+ * @param PyStringNode $data
+ */
+ public function theResponseEqualsPyString(PyStringNode $response)
+ {
+ $this->theResponseEquals($response);
+ }
+
/**
* ============ json array ===================
* @Given /^the response equals (\[[^]]*\])$/
@@ -224,15 +275,6 @@ public function theResponseEquals($response)
}
}
- /**
- * @Given /^the response equals:/
- * @param PyStringNode $data
- */
- public function theResponseEqualsPyString(PyStringNode $response)
- {
- $this->theResponseEquals($response);
- }
-
/**
* @Given /^that I want to make a new "([^"]*)"$/
*/
@@ -253,7 +295,6 @@ public function thatIWantToUpdate($objectType)
$this->_restObjectMethod = 'put';
}
-
/**
* @Given /^that I want to find a "([^"]*)"$/
*/
@@ -283,7 +324,6 @@ public function thatHeaderIsSetTo($header, $value)
$this->_headers[$header] = $value;
}
-
/**
* @Given /^that its "([^"]*)" is "([^"]*)"$/
* @Given /^that his "([^"]*)" is "([^"]*)"$/
@@ -367,14 +407,14 @@ public function iRequest($path)
$this->_request_debug_stream = fopen('php://temp/', 'r+');
- $options = array(
+ $options = [
'headers' => $this->_headers,
'http_errors' => false,
'decode_content' => false,
'debug' => $this->_request_debug_stream,
//'curl' => array(CURLOPT_VERBOSE => true),
- );
- if (in_array($method, array('POST', 'PUT', 'PATCH'))) {
+ ];
+ if (in_array($method, ['POST', 'PUT', 'PATCH'])) {
if (empty($this->_requestBody)) {
$postFields = is_object($this->_restObject)
? (array)$this->_restObject
@@ -401,7 +441,7 @@ public function iRequest($path)
if (strpos($cType, '+') > 0) {
//look for vendor mime
//example 'application/vnd.SomeVendor-v1+json','application/vnd.SomeVendor-v2+json'
- list($app, $vendor, $extension) = [strtok($cType, '/'), strtok('+'), strtok('')];
+ [$app, $vendor, $extension] = [strtok($cType, '/'), strtok('+'), strtok('')];
$cType = "$app/$extension";
}
switch ($cType) {
@@ -438,8 +478,10 @@ public function iRequest($path)
break;
case 'application/xml':
$this->_type = 'xml';
- libxml_use_internal_errors(true);
- libxml_disable_entity_loader(true);
+ @libxml_use_internal_errors(true);
+ if (\LIBXML_VERSION < 20900) {
+ libxml_disable_entity_loader(true);
+ }
$this->_data = @simplexml_load_string(
$this->_response->getBody(true)
);
@@ -475,7 +517,6 @@ public function acceptLanguage($language)
$this->_headers['Accept-Language'] = $language;
}
-
/**
* @Then /^the response is JSON$/
* @Then /^the response should be JSON$/
@@ -532,6 +573,22 @@ public function theResponseLanguageIs($language)
}
}
+ /**
+ * @Then /^the response "Expires" header should be Date\+(\d+) seconds$/
+ */
+ public function theResponseExpiresHeaderShouldBeDatePlusGivenSeconds($seconds)
+ {
+ $server_time = strtotime($this->_response->getHeaderLine('Date')) + $seconds;
+ $expires_time = strtotime($this->_response->getHeaderLine('Expires'));
+ if ($expires_time === $server_time || $expires_time === $server_time + 1) {
+ return;
+ }
+ return $this->theResponseHeaderShouldBe(
+ 'Expires',
+ gmdate('D, d M Y H:i:s \G\M\T', $server_time)
+ );
+ }
+
/**
* @Then /^the response "([^"]*)" header should be "([^"]*)"$/
* @Then /^the response "([^"]*)" header should be '([^']*)'$/
@@ -554,22 +611,6 @@ public function theResponseHeaderShouldBe($header, $value)
}
}
- /**
- * @Then /^the response "Expires" header should be Date\+(\d+) seconds$/
- */
- public function theResponseExpiresHeaderShouldBeDatePlusGivenSeconds($seconds)
- {
- $server_time = strtotime($this->_response->getHeaderLine('Date')) + $seconds;
- $expires_time = strtotime($this->_response->getHeaderLine('Expires'));
- if ($expires_time === $server_time || $expires_time === $server_time + 1) {
- return;
- }
- return $this->theResponseHeaderShouldBe(
- 'Expires',
- gmdate('D, d M Y H:i:s \G\M\T', $server_time)
- );
- }
-
/**
* @Then /^the response time should at least be (\d+) milliseconds$/
*/
@@ -586,7 +627,6 @@ public function theResponseTimeShouldAtLeastBeMilliseconds($milliSeconds)
}
}
-
/**
* @Given /^the type is "([^"]*)"$/
*/
@@ -639,6 +679,15 @@ public function theTypeIs($type)
);
}
+ /**
+ * @Given /^the value equals (\d+)$/
+ */
+ public function theNumericValueEquals($value)
+ {
+ $value = is_float($value) ? floatval($value) : intval($value);
+ return $this->theValueEquals($value);
+ }
+
/**
* @Given /^the value equals "([^"]*)"$/
*/
@@ -656,13 +705,18 @@ public function theValueEquals($value)
}
}
- /**
- * @Given /^the value equals (\d+)$/
- */
- public function theNumericValueEquals($value)
+ private function typeFormat($value): string
{
- $value = is_float($value) ? floatval($value) : intval($value);
- return $this->theValueEquals($value);
+ if (is_null($value)) {
+ return 'null';
+ }
+ if (is_bool($value)) {
+ return $value ? 'true' : 'false';
+ }
+ if (is_array($value) || is_object($value)) {
+ return json_encode($value);
+ }
+ return $value = '"' . $value . '"';
}
/**
@@ -748,6 +802,16 @@ public function theResponseHasAProperty($propertyName)
}
}
+ /**
+ * @Then /^the "([^"]*)" property equals (\d+)$/
+ */
+ public function thePropertyEqualsNumber($propertyName, $propertyValue)
+ {
+ $propertyValue = is_float($propertyValue)
+ ? floatval($propertyValue) : intval($propertyValue);
+ return $this->thePropertyEquals($propertyName, $propertyValue);
+ }
+
/**
* @Then /^the "([^"]*)" property equals "([^"]*)"$/
* @Then /^the "([^"]*)" property equals (null)$/
@@ -792,16 +856,6 @@ public function thePropertyEquals($propertyName, $propertyValue = null)
}
}
- /**
- * @Then /^the "([^"]*)" property equals (\d+)$/
- */
- public function thePropertyEqualsNumber($propertyName, $propertyValue)
- {
- $propertyValue = is_float($propertyValue)
- ? floatval($propertyValue) : intval($propertyValue);
- return $this->thePropertyEquals($propertyName, $propertyValue);
- }
-
/**
* @Then /^the "([^"]*)" property equals (true|false)$/
*/
@@ -860,47 +914,6 @@ public function theResponseStatusCodeShouldBe($httpStatus)
}
}
- /**
- * @Then /^echo last response$/
- */
- public function echoLastResponse()
- {
- global $argv;
- $level = 1;
- if (in_array('-v', $argv) || in_array('--verbose=1', $argv)) {
- $level = 2;
- } elseif (in_array('-vv', $argv) || in_array('--verbose=2', $argv)) {
- $level = 3;
- } elseif (in_array('-vvv', $argv) || in_array('--verbose=3', $argv)) {
- $level = 4;
- }
- //echo "$this->_request\n$this->_response";
- if ($level >= 2 && is_resource($this->_request_debug_stream)) {
- rewind($this->_request_debug_stream);
- echo stream_get_contents($this->_request_debug_stream) . PHP_EOL . PHP_EOL;
- }
- if ($level >= 1) {
- /** @var RequestInterface $req */
- $req = $this->_request;
- echo $req->getMethod() . ' ' . $req->getUri() . ' HTTP/' . $req->getProtocolVersion() . PHP_EOL;
- foreach ($req->getHeaders() as $k => $v) {
- echo ucwords($k) . ': ' . implode(', ', $v) . PHP_EOL;
- }
- echo PHP_EOL;
- echo urldecode((string)$req->getBody()) . PHP_EOL . PHP_EOL;
- }
- /** @var ResponseInterface $res */
- $res = $this->_response;
- echo 'HTTP/' . $res->getProtocolVersion() . ' ' . $res->getStatusCode() . ' ' . $res->getReasonPhrase(
- ) . PHP_EOL;
- foreach ($res->getHeaders() as $k => $v) {
- echo ucwords($k) . ': ' . implode(', ', $v) . PHP_EOL;
- }
- echo PHP_EOL;
- echo (string)$res->getBody();
- }
-
-
/**
* @Given /^the value equals "([^"]*)" or "([^"]*)"$/
*/
@@ -939,18 +952,4 @@ public function theResponseRedirectsTo($expectedPath)
}
}
- private function typeFormat($value): string
- {
- if (is_null($value)) {
- return 'null';
- }
- if (is_bool($value)) {
- return $value ? 'true' : 'false';
- }
- if (is_array($value) || is_object($value)) {
- return json_encode($value);
- }
- return $value = '"' . $value . '"';
- }
-
}
diff --git a/features/examples/_008_documentation.feature b/features/examples/_008_documentation.feature
index c3e7d07..967614e 100644
--- a/features/examples/_008_documentation.feature
+++ b/features/examples/_008_documentation.feature
@@ -56,7 +56,14 @@ Feature: Testing Documentation Example
Then the response status code should be 404
And the response should be JSON
+ @default
Scenario: Checking Redirect of Explorer
When I request "explorer"
Then the response redirects to "explorer/"
And the response should be HTML
+
+ @fpm
+ Scenario: Checking Redirect of Explorer
+ When I request "examples/_008_documentation/explorer"
+ Then the response redirects to "examples/_008_documentation/explorer/"
+ And the response should be HTML
diff --git a/features/examples/_011_versioning.feature b/features/examples/_011_versioning.feature
index fed5ce8..8a1cdee 100644
--- a/features/examples/_011_versioning.feature
+++ b/features/examples/_011_versioning.feature
@@ -10,6 +10,7 @@ Feature: Testing Versioning
And the "message" property equals "Normal weight"
And the "metric.height" property equals "190 centimeters"
+ @default
Scenario: Access version 1 by url
When I request "v1/examples/_011_versioning/bmi?height=190"
Then the response status code should be 200
@@ -19,6 +20,17 @@ Feature: Testing Versioning
And the "message" property equals "Normal weight"
And the "metric.height" property equals "190 centimeters"
+ @fpm
+ Scenario: Access version 1 by url
+ When I request "examples/_011_versioning/v1/bmi?height=190"
+ Then the response status code should be 200
+ And the response is JSON
+ And the type is "array"
+ And the response has a "bmi" property
+ And the "message" property equals "Normal weight"
+ And the "metric.height" property equals "190 centimeters"
+
+ @default
Scenario: Access version 2 by url and passing invalid argument
When I request "v2/examples/_011_versioning/bmi?height=190"
Then the response status code should be 400
@@ -26,40 +38,28 @@ Feature: Testing Versioning
And the type is "array"
And the "error.message" property equals "invalid height unit"
- Scenario: Access version 2 by url
- When I request "v2/examples/_011_versioning/bmi?height=190cm"
- Then the response status code should be 200
+ @fpm
+ Scenario: Access version 2 by url and passing invalid argument
+ When I request "examples/_011_versioning/v2/bmi?height=190"
+ Then the response status code should be 400
And the response is JSON
And the type is "array"
- And the response has a "bmi" property
- And the "message" property equals "Normal weight"
- And the "metric.height" property equals "190 centimeters"
+ And the "error.message" property equals "invalid height unit"
- Scenario: Access version 1 by vendor media type
- Given that "Accept" header is set to "application/vnd.SomeVendor-v1+json"
- When I request "examples/_011_versioning/bmi?height=190"
+ @default
+ Scenario: Access version 2 by url
+ When I request "v2/examples/_011_versioning/bmi?height=190cm"
Then the response status code should be 200
- And the response "Content-Type" header should be "application/vnd.SomeVendor-v1+json; charset=utf-8"
And the response is JSON
And the type is "array"
And the response has a "bmi" property
And the "message" property equals "Normal weight"
And the "metric.height" property equals "190 centimeters"
- Scenario: Access version 2 by vendor media type and passing invalid argument
- Given that "Accept" header is set to "application/vnd.SomeVendor-v2+json"
- When I request "v2/examples/_011_versioning/bmi?height=190"
- Then the response status code should be 400
- And the response "Content-Type" header should be "application/vnd.SomeVendor-v2+json; charset=utf-8"
- And the response is JSON
- And the type is "array"
- And the "error.message" property equals "invalid height unit"
-
- Scenario: Access version 2 by vendor media type
- Given that "Accept" header is set to "application/vnd.SomeVendor-v2+json"
- When I request "v2/examples/_011_versioning/bmi?height=190cm"
+ @fpm
+ Scenario: Access version 2 by url
+ When I request "examples/_011_versioning/v2/bmi?height=190cm"
Then the response status code should be 200
- And the response "Content-Type" header should be "application/vnd.SomeVendor-v2+json; charset=utf-8"
And the response is JSON
And the type is "array"
And the response has a "bmi" property
diff --git a/features/examples/_012_vendor_mime.feature b/features/examples/_012_vendor_mime.feature
new file mode 100644
index 0000000..bb0d38d
--- /dev/null
+++ b/features/examples/_012_vendor_mime.feature
@@ -0,0 +1,33 @@
+@example12 @vendormime
+Feature: Testing Vendor Media Type Versioning
+
+ Scenario: Access version 1 by vendor media type
+ Given that "Accept" header is set to "application/vnd.SomeVendor-v1+json"
+ When I request "examples/_012_vendor_mime/bmi?height=190"
+ Then the response status code should be 200
+ And the response "Content-Type" header should be "application/vnd.SomeVendor-v1+json; charset=utf-8"
+ And the response is JSON
+ And the type is "array"
+ And the response has a "bmi" property
+ And the "message" property equals "Normal weight"
+ And the "metric.height" property equals "190 centimeters"
+
+ Scenario: Access version 2 by vendor media type and passing invalid argument
+ Given that "Accept" header is set to "application/vnd.SomeVendor-v2+json"
+ When I request "examples/_012_vendor_mime/bmi?height=190"
+ Then the response status code should be 400
+ And the response "Content-Type" header should be "application/vnd.SomeVendor-v2+json; charset=utf-8"
+ And the response is JSON
+ And the type is "array"
+ And the "error.message" property equals "invalid height unit"
+
+ Scenario: Access version 2 by vendor media type
+ Given that "Accept" header is set to "application/vnd.SomeVendor-v2+json"
+ When I request "examples/_012_vendor_mime/bmi?height=190cm"
+ Then the response status code should be 200
+ And the response "Content-Type" header should be "application/vnd.SomeVendor-v2+json; charset=utf-8"
+ And the response is JSON
+ And the type is "array"
+ And the response has a "bmi" property
+ And the "message" property equals "Normal weight"
+ And the "metric.height" property equals "190 centimeters"
\ No newline at end of file
diff --git a/interop/Swoole/Convert.php b/interop/Swoole/Convert.php
index 4f6393f..92b0041 100644
--- a/interop/Swoole/Convert.php
+++ b/interop/Swoole/Convert.php
@@ -26,6 +26,7 @@ public final static function toPSR7(Request $request): ServerRequestInterface
);
if ($request->get) {
$instance = $instance->withQueryParams($request->get);
+ $instance = $instance->withUri($instance->getUri()->withQuery(http_build_query($request->get)));
}
if ($request->post) {
$instance = $instance->withParsedBody($request->post);
@@ -39,13 +40,6 @@ public final static function toPSR7(Request $request): ServerRequestInterface
return $instance;
}
- public final static function fromPSR7(ResponseInterface $psr7Response, Response $response): void
- {
- $response->status($psr7Response->getStatusCode());
- static::populateHeaders($psr7Response, $response);
- $response->end((string)$psr7Response->getBody());
- }
-
private static function buildServerParams(Request $request)
{
$server = $request->server ?? [];
@@ -89,6 +83,13 @@ private static function buildServerParams(Request $request)
return $return;
}
+ public final static function fromPSR7(ResponseInterface $psr7Response, Response $response): void
+ {
+ $response->status($psr7Response->getStatusCode());
+ static::populateHeaders($psr7Response, $response);
+ $response->end((string)$psr7Response->getBody());
+ }
+
private static function populateHeaders(ResponseInterface $psr7Response, Response $response)
{
$headers = $psr7Response->getHeaders();
diff --git a/public/index_react.php b/public/index_react.php
index 631f8d2..aa785e2 100644
--- a/public/index_react.php
+++ b/public/index_react.php
@@ -5,29 +5,28 @@
require __DIR__ . '/../api/bootstrap.php';
use Luracast\Restler\Defaults;
+use Luracast\Restler\Middleware\StaticFiles;
use Luracast\Restler\Restler;
use Psr\Http\Message\ServerRequestInterface;
-use React\Http\Middleware\LimitConcurrentRequestsMiddleware;
-use React\Http\Middleware\RequestBodyBufferMiddleware;
-use React\Http\Middleware\RequestBodyParserMiddleware;
-use React\Http\StreamingServer;
+use React\Http\Server;
+//serve static files
+Restler::$middleware[] = new StaticFiles(BASE . '/public');
$loop = React\EventLoop\Factory::create();
ReactHttpClient::setLoop($loop);
Defaults::$implementations[HttpClientInterface::class] = [ReactHttpClient::class];
-$server = new StreamingServer(
- [
- new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
- new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB
- new RequestBodyParserMiddleware(2 * 1024 * 1024, 1), //allow UPLOAD of only 1 file with max 2MB
- function (ServerRequestInterface $request) {
- echo ' ' . $request->getMethod() . ' ' . $request->getUri()->getPath() . PHP_EOL;
- return (new Restler)->handle($request);
- }
- ]
+$server = new Server($loop,
+ new React\Http\Middleware\StreamingRequestMiddleware(),
+ new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
+ new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB per request
+ new React\Http\Middleware\RequestBodyParserMiddleware(),
+ function (ServerRequestInterface $request) {
+ echo ' ' . $request->getMethod() . ' ' . $request->getUri()->getPath() . PHP_EOL;
+ return (new Restler)->handle($request);
+ }
);
$server->on(
@@ -38,7 +37,7 @@ function (Exception $e) {
);
-$socket = new React\Socket\Server(8080, $loop);
+$socket = new React\Socket\Server('0.0.0.0:8080', $loop);
$server->listen($socket);
echo "Server running at http://127.0.0.1:8080\n";
diff --git a/public/index_swoole.php b/public/index_swoole.php
index c503aad..a882d21 100644
--- a/public/index_swoole.php
+++ b/public/index_swoole.php
@@ -3,20 +3,35 @@
use Luracast\Restler\Defaults;
use Luracast\Restler\Restler;
use Psr\Http\Message\ResponseInterface;
+use Swoole\Constant as C;
use Swoole\Http\Convert;
use Swoole\Http\Request;
use Swoole\Http\Response;
+use Swoole\Http\Server;
require __DIR__ . '/../api/bootstrap.php';
+Swoole\Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL | SWOOLE_HOOK_CURL);
+
Defaults::$implementations[HttpClientInterface::class] = [SwooleHttpClient::class];
-$http = new swoole_http_server("127.0.0.1", 8080);
+$http = new Server("127.0.0.1", 8080);
$http->set([
- 'worker_num' => 1, // The number of worker processes
- 'daemonize' => false, // Whether start as a daemon process
- 'backlog' => 128, // TCP backlog connection number
+ C::OPTION_WORKER_NUM => 1, // The number of worker processes
+ C::OPTION_DAEMONIZE => false, // Whether start as a daemon process
+ C::OPTION_BACKLOG => 128, // TCP backlog connection number
+ //Adds support for GZIP compression
+ C::OPTION_HTTP_COMPRESSION => true,
+ C::OPTION_HTTP_COMPRESSION_LEVEL => 5,
+ //Enable static files from public folder
+ C::OPTION_DOCUMENT_ROOT => BASE . '/public',
+ C::OPTION_ENABLE_STATIC_HANDLER => true,
+ /*
+ C::OPTION_STATIC_HANDLER_LOCATIONS => [
+ '/examples',
+ ],
+ */
]);
$http->on('start', function ($server) {
@@ -31,4 +46,4 @@
});
});
-$http->start();
\ No newline at end of file
+$http->start();
diff --git a/public/index_workerman.php b/public/index_workerman.php
index 52d7c93..3ec04b9 100644
--- a/public/index_workerman.php
+++ b/public/index_workerman.php
@@ -1,9 +1,14 @@
count = 4;
@@ -11,4 +16,4 @@
//echo '% ' . $msg . PHP_EOL;
};
// run all workers
-Worker::runAll();
\ No newline at end of file
+Worker::runAll();
diff --git a/src/Auth/JsonWebToken.php b/src/Auth/JsonWebToken.php
new file mode 100644
index 0000000..001ac7a
--- /dev/null
+++ b/src/Auth/JsonWebToken.php
@@ -0,0 +1,107 @@
+ vendor/project:version
+ JWT::class => 'firebase/php-jwt:^5.2'
+ ];
+ }
+
+ public function _isAllowed(ServerRequestInterface $request, ResponseHeaders $responseHeaders): bool
+ {
+ if ($request->hasHeader('authorization') === false) {
+ return false;
+ }
+ try {
+ $header = $request->getHeaderLine('authorization');
+ $jwt = trim((string)preg_replace('/^(?:\s+)?Bearer\s/', '', $header));
+ $this->token = $token = JWT::decode($jwt, static::publicKey(), ['RS256']);
+ $id = $token->{static::$userIdentifierProperty};
+ /** @var UserIdentificationInterface $userClass */
+ $userClass = ClassName::get(UserIdentificationInterface::class);
+ $userClass::setUniqueIdentifier($id);
+ return true;
+ } catch (HttpException $httpException) {
+ throw $httpException;
+ } catch (Throwable $throwable) {
+ $this->accessDenied($throwable->getMessage(), $throwable);
+ }
+ }
+
+ protected static function publicKey(): string
+ {
+ if (empty(self::$publicKey)) {
+ throw new HttpException(500, '`' . static::class . '::$publicKey` is needed for token verification');
+ }
+ $start = "-----BEGIN PUBLIC KEY-----\n";
+ if (0 === strpos(static::$publicKey, $start)) {
+ return static::$publicKey;
+ }
+ return sprintf("%s%s\n-----END PUBLIC KEY-----", $start, wordwrap(
+ static::$publicKey,
+ 64,
+ "\n",
+ true
+ ));
+ }
+
+ /**
+ * @param string $reason
+ * @param ?Throwable $previous
+ * @throws HttpException 403 Access Denied
+ */
+ protected function accessDenied(string $reason, ?Throwable $previous = null)
+ {
+ throw new HttpException(403, 'Access Denied. ' . $reason, [], $previous);
+ }
+}
diff --git a/src/Auth/JsonWebTokenAccessControl.php b/src/Auth/JsonWebTokenAccessControl.php
new file mode 100644
index 0000000..bc097b9
--- /dev/null
+++ b/src/Auth/JsonWebTokenAccessControl.php
@@ -0,0 +1,48 @@
+roles();
+ if (!in_array($this->requires, $roles)) {
+ $this->role = $roles[0];
+ $this->accessDenied('Insufficient Access Rights');
+ }
+ $this->role = $this->requires;
+ return true;
+ }
+
+ /**
+ * @return array|null
+ * @throws HttpException
+ */
+ private function roles(): array
+ {
+ $p = $this->token;
+ foreach (static::$rolesAccessor as $property) {
+ $p = $p->{$property} ?? null;
+ if (!$p) {
+ $this->accessDenied('Roles not specified');
+ }
+ }
+ return $p;
+ }
+}
diff --git a/src/Container.php b/src/Container.php
index a334e1e..4c28841 100644
--- a/src/Container.php
+++ b/src/Container.php
@@ -4,7 +4,7 @@
use Luracast\Restler\Contracts\ContainerInterface;
use Luracast\Restler\Exceptions\ContainerException;
use Luracast\Restler\Exceptions\HttpException;
-use Luracast\Restler\Exceptions\NotFoundException;
+use Luracast\Restler\Exceptions\NotFound;
use Luracast\Restler\Utils\ClassName;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
@@ -49,63 +49,6 @@ public function make($abstract, array $parameters = [])
return $this->resolve($abstract, $parameters);
}
- public function instance($abstract, $instance)
- {
- $this->instances[$abstract] = $instance;
- try {
- if ($class = ClassName::get($abstract)) {
- $this->instances[$class] = $instance;
- }
- } catch (HttpException $e) {
- }
- }
-
- /**
- * Finds an entry of the container by its identifier and returns it.
- *
- * @param string $id Identifier of the entry to look for.
- *
- * @throws NotFoundExceptionInterface No entry was found for **this** identifier.
- * @throws ContainerExceptionInterface Error while retrieving the entry.
- *
- * @return mixed Entry.
- */
- public function get($id)
- {
- try {
- if ($instance = $this->instances[$id] ?? $this->instances[ClassName::get($id)] ?? false) {
- return $instance;
- }
- } catch (\Throwable $t) {
- throw new ContainerException('Error while retrieving the entry `' . $id . '`');
- }
- throw new NotFoundException(' No entry was found for `' . $id . '`` identifier');
- }
-
- /**
- * Returns true if the container can return an entry for the given identifier.
- * Returns false otherwise.
- *
- * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
- * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
- *
- * @param string $id Identifier of the entry to look for.
- *
- * @return bool
- */
- public function has($id)
- {
- if (isset($this->instances[$id])) {
- return true;
- }
- try {
- $class = ClassName::get($id);
- } catch (\Throwable $t) {
- return false;
- }
- return isset($this->instances[$class]);
- }
-
/**
* Build an instance of the given class
*
@@ -158,7 +101,7 @@ public function &resolve(string $abstract, array &$arguments = [])
*/
public function &getDependencies(array $parameters, array &$arguments = [])
{
- $dependencies = array();
+ $dependencies = [];
/**
* @var ReflectionParameter $parameter
*/
@@ -172,7 +115,7 @@ public function &getDependencies(array $parameters, array &$arguments = [])
$byRef
? $dependencies[] = &$arguments[$index]
: $dependencies[] = $arguments[$index];
- } elseif (is_null($dependency = $parameter->getClass())) {
+ } elseif (is_null($dependency = @$parameter->getClass())) {
$byRef
? $dependencies[] = &$this->resolvePrimitive($parameter)
: $dependencies[] = $this->resolvePrimitive($parameter);
@@ -190,24 +133,6 @@ public function &getDependencies(array $parameters, array &$arguments = [])
return $dependencies;
}
- protected function &resolveStaticProperties(ReflectionParameter $parameter)
- {
- if ($value = $this->config[$parameter->name] ?? false) {
- return $value;
- }
- $class = ucfirst($parameter->name);
- if (class_exists($class) || $class = ClassName::get($class) ?? false) {
- $value = $this->config[$parameter->name] = new StaticProperties($class);
- return $value;
- }
- if ($parameter->isDefaultValueAvailable()) {
- $defaultValue = $parameter->getDefaultValue();
- return $defaultValue;
- }
-
- $this->unresolvablePrimitive($parameter);
- }
-
/**
* @param ReflectionParameter $parameter
* @return mixed|null|string
@@ -215,7 +140,7 @@ protected function &resolveStaticProperties(ReflectionParameter $parameter)
*/
protected function &resolvePrimitive(ReflectionParameter $parameter)
{
- if ($parameter->isArray()) {
+ if (@$parameter->isArray()) {
if (($value = $this->config[$parameter->name] ?? false) && is_array($value)) {
return $value;
}
@@ -242,4 +167,79 @@ protected function unresolvablePrimitive(ReflectionParameter $parameter)
"Unresolvable dependency resolving [$parameter] in class {$parameter->getDeclaringClass()->getName()}";
throw new ContainerException($message);
}
+
+ protected function &resolveStaticProperties(ReflectionParameter $parameter)
+ {
+ if ($value = $this->config[$parameter->name] ?? false) {
+ return $value;
+ }
+ $class = ucfirst($parameter->name);
+ if (class_exists($class) || $class = ClassName::get($class) ?? false) {
+ $value = $this->config[$parameter->name] = new StaticProperties($class);
+ return $value;
+ }
+ if ($parameter->isDefaultValueAvailable()) {
+ $defaultValue = $parameter->getDefaultValue();
+ return $defaultValue;
+ }
+
+ $this->unresolvablePrimitive($parameter);
+ }
+
+ public function instance($abstract, $instance)
+ {
+ $this->instances[$abstract] = $instance;
+ try {
+ if ($class = ClassName::get($abstract)) {
+ $this->instances[$class] = $instance;
+ }
+ } catch (HttpException $e) {
+ }
+ }
+
+ /**
+ * Finds an entry of the container by its identifier and returns it.
+ *
+ * @param string $id Identifier of the entry to look for.
+ *
+ * @return mixed Entry.
+ * @throws ContainerExceptionInterface Error while retrieving the entry.
+ *
+ * @throws NotFoundExceptionInterface No entry was found for **this** identifier.
+ */
+ public function get($id)
+ {
+ try {
+ if ($instance = $this->instances[$id] ?? $this->instances[ClassName::get($id)] ?? false) {
+ return $instance;
+ }
+ } catch (\Throwable $t) {
+ throw new ContainerException('Error while retrieving the entry `' . $id . '`');
+ }
+ throw new NotFound(' No entry was found for `' . $id . '`` identifier');
+ }
+
+ /**
+ * Returns true if the container can return an entry for the given identifier.
+ * Returns false otherwise.
+ *
+ * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
+ * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
+ *
+ * @param string $id Identifier of the entry to look for.
+ *
+ * @return bool
+ */
+ public function has($id)
+ {
+ if (isset($this->instances[$id])) {
+ return true;
+ }
+ try {
+ $class = ClassName::get($id);
+ } catch (\Throwable $t) {
+ return false;
+ }
+ return isset($this->instances[$class]);
+ }
}
diff --git a/src/Contracts/DependentTrait.php b/src/Contracts/DependentTrait.php
new file mode 100644
index 0000000..2c8319e
--- /dev/null
+++ b/src/Contracts/DependentTrait.php
@@ -0,0 +1,29 @@
+ $package) {
+ if (!class_exists($className, true)) {
+ throw new HttpException(
+ 500,
+ get_called_class() . ' has external dependency. Please run `composer require ' .
+ $package . '` from the project root. Read https://getcomposer.org for more info'
+ );
+ }
+ }
+ }
+
+ /**
+ * @return array {@type associative}
+ * CLASS_NAME => vendor/project:version
+ */
+ abstract public static function dependencies(): array;
+}
diff --git a/src/Contracts/ValueObjectInterface.php b/src/Contracts/ValueObjectInterface.php
index 499efa8..5e17b5b 100644
--- a/src/Contracts/ValueObjectInterface.php
+++ b/src/Contracts/ValueObjectInterface.php
@@ -1,10 +1,12 @@
_baseUrl = $uri->withPath('')->withQuery('');
} else {
$path = Text::removeCommon($path, ltrim($scriptName, $slash));
+ $base = empty($path) ? $fullPath : substr($fullPath, 0, -strlen($path));
$this->_baseUrl = $uri
- ->withPath(rtrim(substr($fullPath, 0, -strlen($path)), $slash))
+ ->withPath($base)
->withQuery('');
}
if (Defaults::$useUrlBasedVersioning && strlen($path) && $path[0] == 'v') {
@@ -570,6 +570,7 @@ protected function authenticate(ServerRequestInterface $request)
*/
protected function validate()
{
+ $this->_route->apply($this->_route->data, $this->_authenticated);
if (!$this->defaults->autoValidationEnabled) {
return;
}
@@ -584,8 +585,7 @@ protected function validate()
*/
public function call(Route $route)
{
- $access = max($this->defaults->apiAccessLevel, $route->access);
- return $route->handle($access, [$this, 'make']);
+ return $route->handle([$this, 'make']);
}
abstract protected function compose($response = null);
diff --git a/src/Data/ErrorResponse.php b/src/Data/ErrorResponse.php
index 6648b7d..54e83b0 100644
--- a/src/Data/ErrorResponse.php
+++ b/src/Data/ErrorResponse.php
@@ -4,19 +4,20 @@
namespace Luracast\Restler\Data;
-use Exception;
use Luracast\Restler\Contracts\GenericResponseInterface;
use Luracast\Restler\Exceptions\HttpException;
+use Throwable;
class ErrorResponse implements GenericResponseInterface
{
public $response = [];
- public function __construct(Exception $exception, bool $debug = false)
+ public function __construct(Throwable $exception, bool $debug = false)
{
+ $code = $exception->getCode();
$this->response['error'] = [
- 'code' => $exception->getCode(),
- 'message' => $exception->getErrorMessage()
+ 'code' => $code > 99 ? $code : 500,
+ 'message' => $exception->getMessage()
];
if ($exception instanceof HttpException) {
$this->response += $exception->getDetails();
@@ -28,17 +29,12 @@ public function __construct(Exception $exception, bool $debug = false)
}
$trace = array_slice($innerException->getTrace(), 0, 10);
$this->response['debug'] = [
- 'source' => $exception->getSource(),
+ 'source' => !method_exists($exception, 'getSource') ? 'internal' : $exception->getSource(),
'trace' => array_map([static::class, 'simplifyTrace'], $trace)
];
}
}
- public function jsonSerialize()
- {
- return $this->response;
- }
-
public static function responds(string ...$types): Returns
{
return Returns::__set_state([
@@ -120,5 +116,10 @@ private static function simplifyTraceArgs($argument)
}
return $argument;
}
+
+ public function jsonSerialize()
+ {
+ return $this->response;
+ }
}
diff --git a/src/Data/PaginatedResponse.php b/src/Data/PaginatedResponse.php
index d7e9cb6..4923f35 100644
--- a/src/Data/PaginatedResponse.php
+++ b/src/Data/PaginatedResponse.php
@@ -6,45 +6,94 @@
use JsonSerializable;
use Luracast\Restler\Contracts\GenericResponseInterface;
+use Psr\Http\Message\UriInterface;
use ReflectionClass;
+use ReflectionException;
class PaginatedResponse implements GenericResponseInterface
{
- /**
- * @var JsonSerializable
- */
- private $serializable;
+ /** @var array */
+ private $data;
- public function __construct(JsonSerializable $serializable)
+ private function __construct(array $data)
{
- $this->serializable = $serializable;
+ $this->data = $data;
+ }
+
+ public static function fromSerializable(JsonSerializable $prefilled): self
+ {
+ return new static($prefilled->jsonSerialize());
+ }
+
+ public static function fromPrefilled(array $data): self
+ {
+ return new static($data);
+ }
+
+ public static function build(
+ UriInterface $baseUrl,
+ array $collection,
+ int $per_page,
+ int $total,
+ int $current_page
+ ): self {
+ $from = $per_page * ($current_page - 1) + 1;
+ $to = min($total, $from + $per_page - 1);
+ $last_page = ceil($total / $per_page);
+ parse_str($baseUrl->getQuery(), $params);
+ $uri = $baseUrl->withQuery('');
+ $url = function (int $page) use ($params, $uri) {
+ $params['page'] = $page;
+ return (string)$uri->withQuery(http_build_query($params));
+ };
+ return new static([
+ 'total' => $total,
+ 'per_page' => $per_page,
+ 'current_page' => $current_page,
+ 'last_page' => $last_page,
+ 'first_page_url' => $url(1),
+ 'last_page_url' => $url($last_page),
+ 'next_page_url' => $current_page >= $last_page ? null : $url($current_page + 1),
+ 'prev_page_url' => $current_page <= 1 ? null : $url($current_page - 1),
+ 'path' => (string)$uri,
+ 'from' => $from,
+ 'to' => $to,
+ 'data' => $collection,
+ ]);
}
public static function responds(string ...$types): Returns
{
+ try {
+ $data = empty($types)
+ ? Returns::__set_state(['type' => 'object', 'scalar' => false])
+ : Returns::fromClass(new ReflectionClass($types[0]));
+ } catch (ReflectionException $e) {
+ $data = Returns::__set_state(['type' => 'object', 'scalar' => false]);
+ }
+ $data->multiple = true;
+ $data->nullable = false;
return Returns::__set_state([
- 'type' => 'object',
+ 'type' => 'PaginatedResponse',
'properties' => [
+ 'total' => Returns::__set_state(['type' => 'int']),
+ 'per_page' => Returns::__set_state(['type' => 'int']),
'current_page' => Returns::__set_state(['type' => 'int']),
- 'data' => empty($types)
- ? Returns::__set_state(['type' => 'object', 'scalar' => false])
- : Returns::fromClass(new ReflectionClass($types[0])),
- 'first_page_url' => Returns::__set_state(['type' => 'string']),
- 'from' => Returns::__set_state(['type' => 'int']),
'last_page' => Returns::__set_state(['type' => 'int']),
+ 'first_page_url' => Returns::__set_state(['type' => 'string']),
'last_page_url' => Returns::__set_state(['type' => 'string']),
'next_page_url' => Returns::__set_state(['type' => 'string']),
- 'path' => Returns::__set_state(['type' => 'string']),
- 'per_page' => Returns::__set_state(['type' => 'int']),
'prev_page_url' => Returns::__set_state(['type' => 'string']),
+ 'path' => Returns::__set_state(['type' => 'string']),
+ 'from' => Returns::__set_state(['type' => 'int']),
'to' => Returns::__set_state(['type' => 'int']),
- 'total' => Returns::__set_state(['type' => 'int']),
+ 'data' => $data,
]
]);
}
- public function jsonSerialize()
+ public function jsonSerialize(): array
{
- return $this->serializable->jsonSerialize();
+ return $this->data;
}
}
diff --git a/src/Data/Param.php b/src/Data/Param.php
index 902c0e4..3a4ae59 100644
--- a/src/Data/Param.php
+++ b/src/Data/Param.php
@@ -1,8 +1,14 @@
self::ACCESS_PUBLIC,
+ 'protected' => self::ACCESS_PROTECTED,
+ 'private' => self::ACCESS_PRIVATE,
+ ];
+
/**
* Name of the variable being validated
*
@@ -47,9 +64,9 @@ class Param extends Type
*/
public $field;
/**
- * @var mixed default value for the parameter
+ * @var array with hasDefault boolean as the first value and default value for the parameter as second
*/
- public $default;
+ public $default = [false, null];
/**
* @var bool is it required or not
@@ -62,6 +79,11 @@ class Param extends Type
*/
public $from;
+ /**
+ * @var bool variadic parameter, so needs expansion of array
+ */
+ public $variadic = false;
+
/**
* Should we attempt to fix the value?
* When set to false validation class should throw
@@ -159,6 +181,8 @@ class Param extends Type
*/
public $method;
+ public $access = self::ACCESS_PUBLIC;
+
/**
* Instance of the API class currently being called. It will be null most of
* the time. Only when method is defined it will contain an instance.
@@ -176,25 +200,11 @@ public static function fromMethod(ReflectionMethod $method, ?array $doc = null,
return static::fromAbstract($method, $doc, $scope);
}
- public static function fromFunction(ReflectionFunction $function, ?array $doc = null, array $scope = []): array
- {
- if (empty($scope)) {
- $scope = Router::scope($function->getClosureScopeClass());
- }
- return static::fromAbstract($function, $doc, $scope);
- }
-
- public static function fromParameter(ReflectionParameter $parameter, ?array $doc, array $scope): self
- {
- return static::from($parameter, $doc['param'][$parameter->getPosition()] ?? [], $scope);
- }
-
private static function fromAbstract(
ReflectionFunctionAbstract $function,
?array $doc = null,
array $scope = []
- ): array
- {
+ ): array {
if (is_null($doc)) {
try {
$doc = CommentParser::parse($function->getDocComment());
@@ -210,18 +220,30 @@ private static function fromAbstract(
return array_column($params, null, 'name');
}
+ public static function fromParameter(ReflectionParameter $parameter, ?array $doc, array $scope): self
+ {
+ $param = static::from($parameter, $doc['param'][$parameter->getPosition()] ?? [], $scope);
+ if ($parameter->isVariadic()) {
+ $param->multiple = true;
+ $param->variadic = true;
+ }
+ return $param;
+ }
+
protected static function from(?Reflector $reflector, array $metadata = [], array $scope = [])
{
+ $hasDefault = false;
$instance = new static();
$types = $metadata['type'] ?? [];
$properties = $metadata[CommentParser::$embeddedDataName] ?? [];
$itemTypes = $properties['type'] ?? [];
unset($properties['type']);
- $instance->rules = $properties;
$instance->description = $metadata['description'] ?? '';
- if ($reflector && method_exists($reflector, 'isDefaultValueAvailable') && $reflector->isDefaultValueAvailable()) {
+ if ($reflector && method_exists($reflector,
+ 'isDefaultValueAvailable') && $reflector->isDefaultValueAvailable()) {
$default = $reflector->getDefaultValue();
- $instance->default = $default;
+ $instance->default = [true, $default];
+ $hasDefault = true;
$types[] = TypeUtil::fromValue($default);
}
if ($reflector && Defaults::$fullRequestDataName === $reflector->name) {
@@ -233,14 +255,24 @@ protected static function from(?Reflector $reflector, array $metadata = [], arra
} elseif (in_array('array', $types) && empty($itemTypes)) {
array_unshift($itemTypes, 'string');
}
+ if (method_exists($reflector, 'hasType') && $reflector->hasType()) {
+ $reflectionType = $reflector->getType();
+ if ($reflectionType instanceof ReflectionUnionType) {
+ $reflectionTypes = $reflectionType->getTypes();
+ if ('null' === end($reflectionTypes)->getName()) {
+ $metadata['return']['type'][] = 'null';
+ }
+ $reflectionType = $reflectionTypes[0];
+ }
+ }
$instance->apply(
- method_exists($reflector, 'hasType') && $reflector->hasType()
- ? $reflector->getType() : null,
+ $reflectionType ?? null,
$types,
$itemTypes,
$scope
);
- $instance->required = TypeUtil::booleanValue($properties['required'] ?? $reflector && method_exists($reflector, 'isOptional') && !$reflector->isOptional());
+ $instance->required = TypeUtil::booleanValue($properties['required'] ?? $reflector && method_exists($reflector,
+ 'isOptional') && !$reflector->isOptional());
if ($reflector) {
$instance->name = $reflector->getName();
if (method_exists($reflector, 'getPosition')) {
@@ -263,6 +295,8 @@ protected static function from(?Reflector $reflector, array $metadata = [], arra
}
$instance->pattern = $properties['pattern'] ?? null;
$instance->message = $properties['message'] ?? null;
+ $instance->choice = $properties['choice'] ?? null;
+ unset($properties['choice']);
$instance->fix = $properties['fix'] ?? false;
$instance->from = $properties['from']
@@ -276,13 +310,89 @@ protected static function from(?Reflector $reflector, array $metadata = [], arra
?? Router::$formatsByName[$instance->name]
?? null;
}
+ if ($access = self::ACCESS[$properties['access'] ?? ''] ?? false) {
+ unset($properties['access']);
+ if (!$hasDefault) {
+ if (array_key_exists('default', $properties)) {
+ $instance->default = [true, $properties['default']];
+ } elseif ($instance->nullable) {
+ $instance->default = [true, null];
+ } else {
+ throw new Exception('Invalid parameter. private or protected parameter requires ' .
+ 'default value either in the function or with {@default value} comment');
+ }
+ }
+ $instance->access = $access;
+ }
+ $instance->rules = $properties;
return $instance;
}
+ public static function fromFunction(ReflectionFunction $function, ?array $doc = null, array $scope = []): array
+ {
+ if (empty($scope)) {
+ $scope = Router::scope($function->getClosureScopeClass());
+ }
+ return static::fromAbstract($function, $doc, $scope);
+ }
+
public static function filterArray(array $data, bool $onlyNumericKeys): array
{
$callback = $onlyNumericKeys ? 'is_numeric' : 'is_string';
return array_filter($data, $callback, ARRAY_FILTER_USE_KEY);
}
+
+ public function toGraphQL()
+ {
+ if (in_array($this->type, GraphQL::INVALID_TYPES)) {
+ throw new HttpException(500, 'Parameter with data type `' . $this->type . '` is not supported');
+ }
+ $data = [];
+ if (GraphQL::$showDescriptions && $this->description) {
+ $data['description'] = $this->description;
+ }
+ if (!empty($this->choice)) {
+ $keys = $this->rules['select'] ?? $this->choice;
+ if (count($this->choice) !== count($keys)) {
+ throw new HttpException(500, '`@choice` and `@select` items count mismatch');
+ }
+ $type = GraphQL::enum([
+ 'name' => ucfirst($this->name) . 'Enum',
+ 'values' => array_combine($keys, $this->choice),
+ ]);
+ } elseif ($this->scalar) {
+ $type = $this->type !== 'bool' && in_array($this->name, Router::$prefixingParameterNames)
+ ? GraphQLType::id()
+ : call_user_func([GraphQLType::class, $this->type]);
+ if (!$this->required && $this->default[0]) {
+ $data['defaultValue'] = $this->default[1];
+ }
+ } else {
+ $class = ClassName::short($this->type) . 'Input';
+ if (isset(GraphQL::$definitions[$class])) {
+ $type = GraphQL::$definitions[$class];
+ } else {
+ $config = ['name' => $class, 'fields' => []];
+ if (is_array($this->properties)) {
+ /** @var Type $property */
+ foreach ($this->properties as $name => $property) {
+ $config['fields'][$name] = $property->toGraphQL();
+ }
+ }
+ $type = $this instanceof Param
+ ? new InputObjectType($config)
+ : new ObjectType($config);
+ }
+ GraphQL::$definitions[$class] = $type;
+ }
+ if (!$this->nullable) {
+ $type = GraphQLType::nonNull($type);
+ }
+ if ($this->multiple) {
+ $type = GraphQLType::listOf($type);
+ }
+ $data['type'] = $type;
+ return $data;
+ }
}
diff --git a/src/Data/Returns.php b/src/Data/Returns.php
index 3100f4e..9fc73e6 100644
--- a/src/Data/Returns.php
+++ b/src/Data/Returns.php
@@ -4,11 +4,21 @@
namespace Luracast\Restler\Data;
+use GraphQL\Type\Definition\InputObjectType;
+use GraphQL\Type\Definition\ObjectType;
+use GraphQL\Type\Definition\Type as GraphQLType;
+use Luracast\Restler\Exceptions\HttpException;
+use Luracast\Restler\GraphQL\GraphQL;
+use Luracast\Restler\Router;
+use Luracast\Restler\Utils\ClassName;
use Luracast\Restler\Utils\CommentParser;
use ReflectionNamedType;
class Returns extends Type
{
+ /** @var string */
+ public $label;
+
public static function fromReturnType(?ReflectionNamedType $reflectionType, ?array $metadata, array $scope): self
{
$instance = new static();
@@ -16,7 +26,50 @@ public static function fromReturnType(?ReflectionNamedType $reflectionType, ?arr
$itemTypes = $metadata[CommentParser::$embeddedDataName]['type'] ?? ['object'];
$instance->description = $metadata['description'] ?? '';
$instance->format = $metadata[CommentParser::$embeddedDataName]['format'] ?? '';
+ $instance->label = $metadata[CommentParser::$embeddedDataName]['label'] ?? null;
$instance->apply($reflectionType, $types, $itemTypes, $scope);
return $instance;
}
+
+ public function toGraphQL()
+ {
+ if (in_array($this->type, GraphQL::INVALID_TYPES)) {
+ throw new HttpException(500, 'Return value with data type `' . $this->type . '` is not supported');
+ }
+ $type = null;
+ if ($this->scalar) {
+ $type = call_user_func([GraphQLType::class, $this->type]);
+ } else {
+ $class = ClassName::short($this->type);
+ if (isset(GraphQL::$definitions[$class])) {
+ $type = GraphQL::$definitions[$class];
+ } else {
+ $config = ['name' => $class, 'fields' => []];
+ if (is_array($this->properties)) {
+ /** @var Type $property */
+ foreach ($this->properties as $name => $property) {
+ $subType = $property->type !== 'bool' && in_array($name,
+ Router::$prefixingParameterNames)
+ ? GraphQLType::id()
+ : $property->toGraphQL();
+ $config['fields'][$name] = ['type' => $subType];
+ if (GraphQL::$showDescriptions && $property->description) {
+ $config['fields'][$name]['description'] = $property->description;
+ }
+ }
+ }
+ $type = $this instanceof Param
+ ? new InputObjectType($config)
+ : new ObjectType($config);
+ }
+ GraphQL::$definitions[$class] = $type;
+ }
+ if (!$this->nullable) {
+ $type = GraphQLType::nonNull($type);
+ }
+ if ($this->multiple) {
+ $type = GraphQLType::listOf($type);
+ }
+ return $type;
+ }
}
diff --git a/src/Data/Route.php b/src/Data/Route.php
index 7d1b2b3..6ea394a 100644
--- a/src/Data/Route.php
+++ b/src/Data/Route.php
@@ -5,16 +5,25 @@
use GraphQL\Type\Definition\ResolveInfo;
-use Luracast\Restler\Contracts\{RequestMediaTypeInterface,
+use Luracast\Restler\Contracts\{AuthenticationInterface,
+ RequestMediaTypeInterface,
ResponseMediaTypeInterface,
SelectivePathsInterface,
- ValidationInterface};
+ ValidationInterface
+};
+use Luracast\Restler\Defaults;
use Luracast\Restler\Exceptions\HttpException;
+use Luracast\Restler\Exceptions\InvalidAuthCredentials;
use Luracast\Restler\GraphQL\Error;
+use Luracast\Restler\GraphQL\GraphQL;
+use Luracast\Restler\ResponseHeaders;
+use Luracast\Restler\Restler;
use Luracast\Restler\Router;
-use Luracast\Restler\Utils\{ClassName, CommentParser, Type, Validator};
+use Luracast\Restler\Utils\{ClassName, CommentParser, Convert, Type, Validator};
+use Psr\Http\Message\ServerRequestInterface;
use ReflectionFunctionAbstract;
use ReflectionMethod;
+use ReflectionUnionType;
use Throwable;
class Route extends ValueObject
@@ -24,6 +33,12 @@ class Route extends ValueObject
const ACCESS_PROTECTED_BY_COMMENT = 2;
const ACCESS_PROTECTED_METHOD = 3;
+ const ACCESS = [
+ 'public' => self::ACCESS_PUBLIC,
+ 'hybrid' => self::ACCESS_HYBRID,
+ 'protected' => self::ACCESS_PROTECTED_BY_COMMENT
+ ];
+
const PROPERTY_TAGS = [
'query',
@@ -125,7 +140,7 @@ class Route extends ValueObject
public $deprecated = false;
- public $resource = ['summary' => '', 'description' => ''];
+ public $resource = ['path' => '', 'summary' => '', 'description' => ''];
/**
* @var array
@@ -187,18 +202,28 @@ public static function fromMethod(ReflectionMethod $method, ?array $metadata = n
}
}
}
- $methodUrl = strtolower($method->getName());
- if ($url = $metadata['url'][0] ?? false) {
- $route->httpMethod = strtoupper(strtok($url, ' '));
- } elseif (preg_match_all('/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/i', $methodUrl, $matches)) {
+ //$methodName = $metadata['url'][0] ?? $method->getName();
+ //$methodName = Text::slug(strtok($methodName, '/'),'');
+ $methodName = $method->getName();
+
+ if (preg_match_all('/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/i', $methodName, $matches)) {
$route->httpMethod = strtoupper($matches[0][0]);
+ $methodName = substr($methodName, strlen($route->httpMethod));
} else {
$route->httpMethod = 'GET';
}
-
+ $route->url = str_replace('index', '', $methodName);
$route->action = [$method->class, $method->getName()];
+ $reflectionType = $method->hasReturnType() ? $method->getReturnType() : null;
+ if ($reflectionType instanceof ReflectionUnionType) {
+ $types = $reflectionType->getTypes();
+ if('null' === end($types)->getName()){
+ $metadata['return']['type'][] = 'null';
+ }
+ $reflectionType = $types[0];
+ }
$route->return = Returns::fromReturnType(
- $method->hasReturnType() ? $method->getReturnType() : null,
+ $reflectionType,
$metadata['return'] ?? ['type' => ['array']],
$scope
);
@@ -264,24 +289,42 @@ function ($matches) use (&$pathParams, $instance) {
return $instance;
}
- public function setRequestMediaTypes(string ...$types): void
+ public function filterParams(bool $include, string $from = Param::FROM_BODY): array
{
- Router::_setMediaTypes(
- RequestMediaTypeInterface::class,
- $types,
- $this->requestFormatMap,
- $this->requestMediaTypes
+ return array_filter(
+ $this->parameters,
+ function ($v) use ($from, $include) {
+ return $include ? $from === $v->from : $from !== $v->from;
+ }
);
}
- public function setResponseMediaTypes(string ...$types): void
+ private function setAuthAndFilters(): void
{
- Router::_setMediaTypes(
- ResponseMediaTypeInterface::class,
- $types,
- $this->responseFormatMap,
- $this->responseMediaTypes
- );
+ foreach (Router::$preAuthFilterClasses as $preFilter) {
+ if (Type::implements($preFilter, SelectivePathsInterface::class)) {
+ if (!$preFilter::isPathSelected($this->path)) {
+ continue;
+ }
+ }
+ $this->preAuthFilterClasses[] = $preFilter;
+ }
+ foreach (Router::$authClasses as $authClass) {
+ if (Type::implements($authClass, SelectivePathsInterface::class)) {
+ if (!$authClass::isPathSelected($this->path)) {
+ continue;
+ }
+ }
+ $this->authClasses[] = $authClass;
+ }
+ foreach (Router::$postAuthFilterClasses as $postFilter) {
+ if (Type::implements($postFilter, SelectivePathsInterface::class)) {
+ if (!$postFilter::isPathSelected($this->path)) {
+ continue;
+ }
+ }
+ $this->postAuthFilterClasses[] = $postFilter;
+ }
}
public function addParameter(Param $parameter)
@@ -290,28 +333,135 @@ public function addParameter(Param $parameter)
$this->parameters[$parameter->name] = $parameter;
}
- public function call(array $arguments, bool $validate = true, callable $maker = null)
+ public function __clone()
+ {
+ $this->parameters = array_map(function ($param) {
+ return clone $param;
+ }, $this->parameters);
+ $this->return = clone $this->return;
+ }
+
+ /**
+ * @return array
+ */
+ public function getArguments(): array
+ {
+ return $this->arguments;
+ }
+
+ public function toGraphQL(): array
+ {
+ $config = [
+ 'type' => $this->return->toGraphQL(),
+ 'args' => [],
+ 'resolve' => function ($root, $args, array $context, ResolveInfo $info) {
+ try {
+ /** @var Restler $restler */
+ $restler = $context['restler'];
+ $authenticated = $this->authenticate(
+ $context['request'],
+ $restler->responseHeaders,
+ $context['maker'],
+ max($this->access, GraphQL::$apiAccessLevel ?? Defaults::$apiAccessLevel)
+ );
+ $context['root'] = $root;
+ $context['info'] = $info;
+ /** @var Convert $convert */
+ $convert = $context['maker'](Convert::class);
+ return $convert->toArray($this->call($args, $authenticated, true, $context['maker']));
+ } catch (Throwable $throwable) {
+ $source = strtolower(pathinfo($throwable->getFile(), PATHINFO_FILENAME));
+ throw new Error($source, $throwable);
+ }
+ }
+ ];
+ /**
+ * @var string $name
+ * @var Type $param
+ */
+ foreach ($this->parameters as $name => $param) {
+ $config['args'][$name] = $param->toGraphQL();
+ }
+ return $config;
+ }
+
+ public function authenticate(
+ ServerRequestInterface $request,
+ ResponseHeaders $responseHeaders,
+ callable $maker,
+ ?int $accessLevel = null
+ ): bool {
+ if (is_null($accessLevel)) {
+ $accessLevel = $this->access;
+ }
+ if (!$accessLevel) {
+ return false;
+ }
+ if ($accessLevel > self::ACCESS_HYBRID && empty($this->authClasses)) {
+ throw new HttpException(
+ 403,
+ 'access denied. no applicable authentication class.'
+ );
+ }
+ $unauthorized = false;
+ foreach ($this->authClasses as $i => $authClass) {
+ try {
+ /** @var AuthenticationInterface $auth */
+ $auth = call_user_func($maker, $authClass, $this);
+ if (!$auth->_isAllowed($request, $responseHeaders)) {
+ throw new HttpException(401, null, ['from' => $authClass]);
+ }
+ $unauthorized = false;
+ //make this auth class as the first one
+ array_splice($this->authClasses, $i, 1);
+ array_unshift($this->authClasses, $authClass);
+ break;
+ } catch (InvalidAuthCredentials $e) { //provided credentials does not authenticate
+ throw $e;
+ } catch (HttpException $e) {
+ if (!$unauthorized) {
+ $unauthorized = $e;
+ }
+ }
+ }
+ if ($accessLevel > self::ACCESS_HYBRID && $unauthorized) {
+ throw $unauthorized;
+ }
+ return $unauthorized ? false : true;
+ }
+
+ public function call(array $arguments, bool $authenticated = false, bool $validate = true, callable $maker = null)
{
if (!$maker) {
$maker = function ($class) {
return new $class;
};
}
- $this->apply($arguments);
+ $this->apply($arguments, $authenticated);
if ($validate) {
$this->validate($maker(Validator::class), $maker);
}
- return $this->handle(1, $maker);
+ return $this->handle($maker);
}
- public function apply(array $arguments): array
+ public function apply(array $arguments, bool $authenticated = false): array
{
$p = [];
foreach ($this->parameters as $parameter) {
- $p[$parameter->index] = $arguments[$parameter->name]
- ?? $arguments[$parameter->index]
- ?? $parameter->default
- ?? null;
+ if (
+ Param::ACCESS_PRIVATE === $parameter->access ||
+ (!$authenticated && Param::ACCESS_PROTECTED === $parameter->access)
+ ) {
+ $p[$parameter->index] = $parameter->default[1];
+ } elseif ($parameter->variadic) {
+ $p[$parameter->index] = $arguments[$parameter->name]
+ ?? array_slice($arguments, $parameter->index);
+ } else {
+ $p[$parameter->index] = $arguments[$parameter->name]
+ ?? $arguments[$parameter->index]
+ ?? $parameter->default[1]
+ ?? null;
+ }
}
if (empty($p) && !empty($arguments)) {
$this->arguments = array_values($arguments);
@@ -341,18 +491,22 @@ public function validate(ValidationInterface $validator, callable $maker)
}
}
- public function __clone()
- {
- $this->parameters = array_map(function ($param) {
- return clone $param;
- }, $this->parameters);
- $this->return = clone $this->return;
- }
-
- public function handle(int $access, callable $maker)
+ public function handle(callable $maker)
{
+ $arguments = [];
+ if ($this->parameters) {
+ foreach ($this->parameters as $param) {
+ $argument = $this->arguments[$param->index];
+ //expand variadic parameters
+ $param->variadic
+ ? $arguments = array_merge($arguments, $argument)
+ : $arguments [] = $argument;
+ }
+ } else {
+ $arguments = $this->arguments;
+ }
$action = $this->action;
- switch ($access) {
+ switch ($this->access) {
case self::ACCESS_PROTECTED_METHOD:
$object = $maker($action[0]);
$reflectionMethod = new ReflectionMethod(
@@ -362,105 +516,54 @@ public function handle(int $access, callable $maker)
$reflectionMethod->setAccessible(true);
return $reflectionMethod->invokeArgs(
$object,
- $this->arguments
+ $arguments
);
default:
if (is_array($action) && count($action) && is_string($action[0]) && class_exists($action[0])) {
$action[0] = $maker($action[0]);
}
- return call_user_func_array($action, $this->arguments);
- }
- }
-
- public function filterParams(bool $include, string $from = Param::FROM_BODY): array
- {
- return array_filter(
- $this->parameters,
- function ($v) use ($from, $include) {
- return $include ? $from === $v->from : $from !== $v->from;
- }
- );
- }
-
- /**
- * @return array
- */
- public function getArguments(): array
- {
- return $this->arguments;
- }
-
- public function toGraphQL(): array
- {
- $config = [
- 'type' => $this->return->toGraphQL(),
- 'args' => [],
- 'resolve' => function ($root, $args, array $context, ResolveInfo $info) {
- try {
- $context['root'] = $root;
- $context['info'] = $info;
- return $this->call($args, true, $context['maker']);
- } catch (Throwable $throwable) {
- $source = strtolower(pathinfo($throwable->getFile(), PATHINFO_FILENAME));
- throw new Error($source, $throwable);
- }
- }
- ];
- /**
- * @var string $name
- * @var Type $param
- */
- foreach ($this->parameters as $name => $param) {
- $config['args'][$name] = $param->toGraphQL();
+ return call_user_func_array($action, $arguments);
}
- return $config;
}
- private function setAuthAndFilters(): void
+ public function __toString()
{
- foreach (Router::$preAuthFilterClasses as $preFilter) {
- if (Type::implements($preFilter, SelectivePathsInterface::class)) {
- if (!$preFilter::isPathSelected($this->path)) {
- continue;
- }
- }
- $this->preAuthFilterClasses[] = $preFilter;
- }
- foreach (Router::$authClasses as $authClass) {
- if (Type::implements($authClass, SelectivePathsInterface::class)) {
- if (!$authClass::isPathSelected($this->path)) {
- continue;
- }
+ if (is_array($this->action)) {
+ $action = $this->action;
+ if (!is_string($action[0])) {
+ $action[0] = get_class($action[0]);
}
- $this->authClasses[] = $authClass;
+ return implode('::', $action) . '()';
}
- foreach (Router::$postAuthFilterClasses as $postFilter) {
- if (Type::implements($postFilter, SelectivePathsInterface::class)) {
- if (!$postFilter::isPathSelected($this->path)) {
- continue;
- }
- }
- $this->postAuthFilterClasses[] = $postFilter;
+ if (is_string($this->action)) {
+ return $this->action . '()';
}
+ return 'closure()';
}
- private function setAccess(string $name, ?string $access = null, ReflectionFunctionAbstract $function, ?array $metadata = null, array $scope = []): void
- {
+ private function setAccess(
+ string $name,
+ ?string $access = null,
+ ReflectionFunctionAbstract $function,
+ ?array $metadata = null,
+ array $scope = []
+ ): void {
if ($function->isProtected()) {
$this->access = self::ACCESS_PROTECTED_METHOD;
- } elseif (is_string($access)) {
- if ('protected' == $access) {
- $this->access = self::ACCESS_PROTECTED_BY_COMMENT;
- } elseif ('hybrid' == $access) {
- $this->access = self::ACCESS_HYBRID;
- }
+ } elseif ($access = self::ACCESS[$access ?? ''] ?? null) {
+ $this->access = $access;
} elseif (isset($metadata['protected'])) {
$this->access = self::ACCESS_PROTECTED_BY_COMMENT;
}
}
- private function setClassProperties(string $name, ?array $class = null, ReflectionFunctionAbstract $function, ?array $metadata = null, array $scope = []): void
- {
+ private function setClassProperties(
+ string $name,
+ ?array $class = null,
+ ReflectionFunctionAbstract $function,
+ ?array $metadata = null,
+ array $scope = []
+ ): void {
$classes = $class ?? [];
foreach ($classes as $class => $value) {
$class = ClassName::resolve($class, $scope);
@@ -471,9 +574,16 @@ private function setClassProperties(string $name, ?array $class = null, Reflecti
}
}
- private function overrideFormats(string $name, ?array $formats = null, ReflectionFunctionAbstract $function, ?array $metadata = null, array $scope = []): void
- {
- if (!$formats) return;
+ private function overrideFormats(
+ string $name,
+ ?array $formats = null,
+ ReflectionFunctionAbstract $function,
+ ?array $metadata = null,
+ array $scope = []
+ ): void {
+ if (!$formats) {
+ return;
+ }
$overrides = [];
$resolver = function ($value) use ($scope, &$overrides) {
$value = ClassName::resolve(trim($value), $scope);
@@ -510,4 +620,24 @@ private function overrideFormats(string $name, ?array $formats = null, Reflectio
}
}
+
+ public function setRequestMediaTypes(string ...$types): void
+ {
+ Router::_setMediaTypes(
+ RequestMediaTypeInterface::class,
+ $types,
+ $this->requestFormatMap,
+ $this->requestMediaTypes
+ );
+ }
+
+ public function setResponseMediaTypes(string ...$types): void
+ {
+ Router::_setMediaTypes(
+ ResponseMediaTypeInterface::class,
+ $types,
+ $this->responseFormatMap,
+ $this->responseMediaTypes
+ );
+ }
}
diff --git a/src/Data/Type.php b/src/Data/Type.php
index b572387..d8c4cf9 100644
--- a/src/Data/Type.php
+++ b/src/Data/Type.php
@@ -4,16 +4,11 @@
namespace Luracast\Restler\Data;
+use Error;
use Exception;
-
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type as GraphQLType;
-
use Luracast\Restler\Contracts\GenericRequestInterface;
use Luracast\Restler\Contracts\GenericResponseInterface;
-use Luracast\Restler\Contracts\ValueObjectInterface;
use Luracast\Restler\Exceptions\Invalid;
-use Luracast\Restler\GraphQL\GraphQL;
use Luracast\Restler\Router;
use Luracast\Restler\Utils\ClassName;
use Luracast\Restler\Utils\CommentParser;
@@ -23,8 +18,42 @@
use ReflectionType;
use Reflector;
-class Type implements ValueObjectInterface
+/**
+ * @method static string() creates a string
+ * @method static nullableString() creates a nullable string
+ * @method static stringArray() creates an array of strings
+ * @method static nullableStringArray() creates a nullable array of strings
+ *
+ * @method static int() creates an integer
+ * @method static nullableInt() creates a nullable integer
+ * @method static intArray() creates an array of integers
+ * @method static nullableIntArray() creates a nullable array of integers
+ *
+ * @method static float() creates a floating point number
+ * @method static nullableFloat() creates a nullable floating point number
+ * @method static floatArray() creates an array of floating point numbers
+ * @method static nullableFloatArray() creates a nullable array of floating point numbers
+ *
+ * @method static object(string $className, array $properties) creates an object with properties
+ * @method static nullableObject(string $className, array $properties) creates a nullable object with properties
+ * @method static objectArray(string $className, array $properties) creates an array of objects with given properties
+ * @method static nullableObjectArray(string $className, array $properties) creates a nullable array of objects with given properties
+ */
+abstract class Type extends ValueObject
{
+ public const SCALAR = [
+ 'int' => 'integer',
+ 'integer' => 'integer',
+ 'bool' => 'boolean',
+ 'boolean' => 'boolean',
+ 'float' => 'float',
+ 'string' => 'string'
+ ];
+
+ const NULLABLE = 0;
+ const NOT_NULLABLE = 1;
+ const DETECT_NULLABLE = 3;
+
/**
* Data type of the variable being validated.
* It will be mostly string
@@ -70,46 +99,41 @@ class Type implements ValueObjectInterface
*/
public $reference = null;
-
- /**
- * @inheritDoc
- */
- public function __toString()
- {
- $str = '';
- if ($this->nullable) $str .= '?';
- $str .= $this->type;
- if ($this->multiple) $str .= '[]';
- $str .= '; // ' . get_called_class();
- if (!$this->scalar) $str = 'new ' . $str;
- return $str;
- }
-
- /**
- * @inheritDoc
- */
- public function jsonSerialize()
- {
- return array_filter(get_object_vars($this));
- }
-
- public function __debugInfo()
- {
- return $this->jsonSerialize();
- }
-
- public function __sleep()
+ public static function fromProperty(?ReflectionProperty $property, ?array $doc = null, array $scope = [])
{
- return $this->jsonSerialize();
+ if ($doc) {
+ $var = $doc;
+ } else {
+ try {
+ $var = CommentParser::parse($property->getDocComment() ?? '')['var']
+ ?? ['type' => ['string']];
+ } catch (Exception $e) {
+ //ignore
+ }
+ }
+ return static::from($property, $var, $scope);
}
/**
- * @inheritDoc
+ * @param Reflector|null $reflector
+ * @param array $metadata
+ * @param array $scope
+ * @return static
*/
- public static function __set_state(array $properties)
+ protected static function from(?Reflector $reflector, array $metadata = [], array $scope = [])
{
$instance = new static();
- $instance->applyProperties($properties);
+ $types = $metadata['type'] ?? [];
+ $itemTypes = $metadata[CommentParser::$embeddedDataName]['type'] ?? [];
+ $instance->description = $metadata['description'] ?? '';
+ $instance->apply(
+ method_exists($reflector, 'hasType') && $reflector->hasType()
+ ? $reflector->getType() : null,
+ $types,
+ $itemTypes,
+ $scope
+ );
+
return $instance;
}
@@ -154,67 +178,19 @@ protected function apply(?ReflectionType $reflectionType, array $types, array $s
$type = call_user_func_array([$class->name, $method], $generics);
$this->properties = $type->properties;
$this->type = $type->type;
+ $this->multiple = $type->multiple;
+ $this->nullable = $type->nullable;
} else {
$this->properties = static::propertiesFromClass($class);
}
}
}
- /**
- * @param Reflector|null $reflector
- * @param array $metadata
- * @param array $scope
- * @return static
- */
- protected static function from(?Reflector $reflector, array $metadata = [], array $scope = [])
- {
- $instance = new static();
- $types = $metadata['type'] ?? [];
- $itemTypes = $metadata[CommentParser::$embeddedDataName]['type'] ?? [];
- $instance->description = $metadata['description'] ?? '';
- $instance->apply(
- method_exists($reflector, 'hasType') && $reflector->hasType()
- ? $reflector->getType() : null,
- $types,
- $itemTypes,
- $scope
- );
-
- return $instance;
- }
-
- public static function fromProperty(?ReflectionProperty $property, ?array $doc = null, array $scope = [])
- {
- if ($doc) {
- $var = $doc;
- } else {
- try {
- $var = CommentParser::parse($property->getDocComment() ?? '')['var']
- ?? ['type' => ['string']];
- } catch (Exception $e) {
- //ignore
- }
- }
- return static::from($property, $var, $scope);
- }
-
- public static function fromClass(ReflectionClass $reflectionClass)
- {
- $isParameter = Param::class === get_called_class();
- $interface = $isParameter ? GenericRequestInterface::class : GenericResponseInterface::class;
- $method = $isParameter ? 'requests' : 'responds';
- if ($reflectionClass->implementsInterface($interface)) {
- return call_user_func([$reflectionClass->name, $method]);
- }
- $instance = new static;
- $instance->scalar = false;
- $instance->type = $reflectionClass->name;
- $instance->properties = self::propertiesFromClass($reflectionClass);
- return $instance;
- }
-
- protected static function propertiesFromClass(ReflectionClass $reflectionClass, array $selectedProperties = [], array $requiredProperties = [])
- {
+ protected static function propertiesFromClass(
+ ReflectionClass $reflectionClass,
+ array $selectedProperties = [],
+ array $requiredProperties = []
+ ) {
$isParameter = Param::class === get_called_class();
$filter = !empty($selectedProperties);
$properties = [];
@@ -240,6 +216,9 @@ protected static function propertiesFromClass(ReflectionClass $reflectionClass,
if (empty($magicProperties)) {
$reflectionProperties = $reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC);
foreach ($reflectionProperties as $reflectionProperty) {
+ if ($reflectionProperty->isStatic()) {
+ continue;
+ }
$name = $reflectionProperty->getName();
if ($filter && !in_array($name, $selectedProperties)) {
continue;
@@ -260,80 +239,156 @@ protected static function propertiesFromClass(ReflectionClass $reflectionClass,
return $properties;
}
- public static function fromSampleData(array $data)
+ public static function fromClass(ReflectionClass $reflectionClass)
{
- if (empty($data)) {
- throw new Invalid('data can\'t be empty');
+ $isParameter = Param::class === get_called_class();
+ $interface = $isParameter ? GenericRequestInterface::class : GenericResponseInterface::class;
+ $method = $isParameter ? 'requests' : 'responds';
+ if ($reflectionClass->implementsInterface($interface)) {
+ return call_user_func([$reflectionClass->name, $method]);
}
- $properties = Param::filterArray($data, Param::KEEP_NON_NUMERIC);
- if (empty($properties)) {
- //array of items
- /** @var Type $value */
- $value = static::fromSampleData($data[0]);
- $value->multiple = true;
- return $value;
+ $instance = new static;
+ $instance->scalar = false;
+ $instance->type = $reflectionClass->name;
+ $instance->properties = self::propertiesFromClass($reflectionClass);
+ return $instance;
+ }
+
+ public static function fromSampleData($data, ?string $name = null, int $nullability = self::NOT_NULLABLE)
+ {
+ if (is_null($data) || (is_array($data) && empty($data))) {
+ throw new Invalid('data can\'t be empty');
}
/** @var Type $obj */
$obj = static::fromValue($data);
- foreach ($properties as $name => $value) {
- $obj->properties[$name] = static::fromValue($value);
+ if (is_array($data)) {
+ if (empty($name)) {
+ throw new Invalid('name can\'t be empty for object type');
+ }
+ $properties = Param::filterArray($data, Param::KEEP_NON_NUMERIC);
+ if (empty($properties)) {
+ //array of items
+ /** @var Type $value */
+ $value = static::fromSampleData($data[0], $name);
+ $value->multiple = true;
+ return $value;
+ }
+ foreach ($properties as $key => $value) {
+ $obj->properties[$key] = static::fromSampleData($value, $name . ucfirst($key));
+
+ }
+ $obj->type = $name;
+ }
+ switch ($nullability) {
+ case self::NULLABLE:
+ $obj->nullable = true;
+ break;
+ case self::NOT_NULLABLE:
+ $obj->nullable = false;
+ break;
+ default:
+ $obj->nullable = !(bool)$data;
}
return $obj;
}
- public static function fromValue($value): Type
+ public static function fromValue($value, string $name = 'object'): Type
{
$instance = new static();
if (is_scalar($value)) {
$instance->scalar = true;
if (is_numeric($value)) {
$instance->type = is_float($value) ? 'float' : 'int';
+ } elseif (is_bool($value)) {
+ $instance->type = 'boolean';
+ } elseif (is_null($value)) {
+ $instance->nullable = true;
+ $instance->type = 'string';
} else {
$instance->type = 'string';
}
} else {
$instance->scalar = false;
- $instance->type = 'object';
+ $instance->type = $name;
}
return $instance;
}
- protected function applyProperties(array $properties, bool $filter = true)
+ public static function make(string $type, bool $multiple = false, bool $nullable = false)
{
- if ($filter) {
- $vars = get_object_vars($this);
- $filtered = array_intersect_key($properties, $vars);
- } else {
- $filtered = $properties;
- }
- foreach ($filtered as $k => $v) if (!is_null($v)) $this->{$k} = $v;
+ $instance = static::__set_state(compact('type', 'multiple', 'nullable'));
+ $instance->scalar = TypeUtil::isScalar($type);
+ return $instance;
}
- public function toGraphQL(): GraphQLType
+ public static function __callStatic($name, $arguments)
{
- $type = null;
- if ($this->scalar) {
- $type = call_user_func([GraphQLType::class, $this->type]);
- } elseif (isset(GraphQL::$definitions[$this->type])) {
- $type = GraphQL::$definitions[$this->type];
- } else {
- $config = ['name' => $this->type, 'fields' => []];
- if (is_array($this->properties)) {
- /** @var Type $property */
- foreach ($this->properties as $name => $property) {
- $config['fields'][$name] = $property->toGraphQL();
+ $parts = array_map('strtolower', preg_split('/(?=[A-Z])/', $name));
+ $type = array_pop($parts);
+ if ('array' == $type) {
+ array_unshift($parts, $type);
+ $type = array_pop($parts);
+ }
+ $data = [];
+ if ('object' === $type && !empty($arguments) && 2 == count($arguments)) {
+ [$name, $properties] = $arguments;
+ $data['type'] = $name;
+ $data['scalar'] = false;
+ $data['properties'] = [];
+ if (is_array($properties)) {
+ foreach ($properties as $key => $value) {
+ $var = is_array($value) ? array_shift($value) : $value;
+ $args = is_array($value) ? $value : [];
+ $data['properties'][$key] = call_user_func([static::class, __FUNCTION__], $var, $args);
}
}
- $type = new ObjectType($config);
- GraphQL::$definitions[$this->type] = $type;
+ } elseif ($type = self::SCALAR[$type] ?? false) {
+ $data['type'] = $type;
+ $data['scalar'] = true;
+ } else {
+ throw new Error(sprintf(
+ "Call to undefined method %s::%s()",
+ static::class,
+ $name
+ ));
}
- if (!$this->nullable) {
- $type = GraphQLType::nonNull($type);
+ $instance = new static();
+ $instance->applyProperties($data, true);
+ $instance->nullable = in_array('nullable', $parts);
+ $instance->multiple = in_array('multiple', $parts) || in_array('array', $parts);
+ return $instance;
+ }
+
+ abstract public function toGraphQL();
+
+ /**
+ * @inheritDoc
+ */
+ public function __toString(): string
+ {
+ $str = '';
+ if ($this->nullable) {
+ $str .= '?';
}
+ $str .= $this->type;
if ($this->multiple) {
- $type = GraphQLType::listOf($type);
+ $str .= '[]';
}
- return $type;
+ $str .= '; // ' . get_called_class();
+ if (!$this->scalar) {
+ $str = 'new ' . $str;
+ }
+ return $str;
+ }
+
+ public function __sleep(): array
+ {
+ return $this->jsonSerialize();
+ }
+
+ public function jsonSerialize(): array
+ {
+ return array_filter(parent::jsonSerialize());
}
}
diff --git a/src/Data/ValueObject.php b/src/Data/ValueObject.php
index fad60d2..3a76f2a 100644
--- a/src/Data/ValueObject.php
+++ b/src/Data/ValueObject.php
@@ -9,9 +9,17 @@
class ValueObject implements ValueObjectInterface
{
- public function __toString()
+ /**
+ * @param array $properties
+ * @return static
+ */
+ public static function __set_state(array $properties)
{
- return ' new ' . get_called_class() . '() ';
+ $class = get_called_class();
+ /** @var ValueObject $instance */
+ $instance = new $class ();
+ $instance->applyProperties($properties);
+ return $instance;
}
protected function applyProperties(array $properties, bool $filter = false)
@@ -26,10 +34,10 @@ protected function applyProperties(array $properties, bool $filter = false)
} else {
$method = 'set' . ucfirst($property);
if (method_exists($this, $method)) {
- call_user_func(array(
+ call_user_func([
$this,
$method
- ), $value);
+ ], $value);
}
}
}
@@ -41,17 +49,14 @@ protected function applyProperties(array $properties, bool $filter = false)
}
}
- /**
- * @param array $properties
- * @return static
- */
- public static function __set_state(array $properties)
+ public function __toString()
{
- $class = get_called_class();
- /** @var ValueObject $instance */
- $instance = new $class ();
- $instance->applyProperties($properties);
- return $instance;
+ return ' new ' . get_called_class() . '() ';
+ }
+
+ public function __debugInfo()
+ {
+ return $this->jsonSerialize();
}
public function jsonSerialize(): array
@@ -65,5 +70,6 @@ public function jsonSerialize(): array
}
return $r;
}
+
}
diff --git a/src/Exceptions/HttpException.php b/src/Exceptions/HttpException.php
index b203cf8..2d8c499 100644
--- a/src/Exceptions/HttpException.php
+++ b/src/Exceptions/HttpException.php
@@ -9,7 +9,7 @@ class HttpException extends Exception
*
* @var array
*/
- public static $codes = array(
+ public static $codes = [
100 => 'Continue',
101 => 'Switching Protocols',
200 => 'OK',
@@ -52,10 +52,10 @@ class HttpException extends Exception
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported'
- );
+ ];
+ public $emptyMessageBody = false;
private $details = [];
private $headers = [];
- public $emptyMessageBody = false;
/**
* HttpException constructor.
@@ -65,12 +65,11 @@ class HttpException extends Exception
* @param null $previous
*/
public function __construct(
- int $httpStatusCode,
+ int $httpStatusCode = 500,
?string $errorMessage = null,
- array $details = array(),
+ array $details = [],
$previous = null
- )
- {
+ ) {
$errorMessage = $errorMessage ?? static::$codes[$httpStatusCode] ?? null;
$this->details = $details;
parent::__construct($errorMessage, $httpStatusCode, $previous);
diff --git a/src/Exceptions/NotFoundException.php b/src/Exceptions/NotFound.php
similarity index 79%
rename from src/Exceptions/NotFoundException.php
rename to src/Exceptions/NotFound.php
index 27dd361..8e46860 100644
--- a/src/Exceptions/NotFoundException.php
+++ b/src/Exceptions/NotFound.php
@@ -4,10 +4,10 @@
use Luracast\Restler\Exceptions\HttpException;
use Psr\Container\NotFoundExceptionInterface;
-class NotFoundException extends HttpException implements NotFoundExceptionInterface
+class NotFound extends HttpException implements NotFoundExceptionInterface
{
public function __construct(?string $errorMessage = null, array $details = array(), $previous = null)
{
parent::__construct(404, $errorMessage, $details, $previous);
}
-}
\ No newline at end of file
+}
diff --git a/src/GraphQL/GraphQL.php b/src/GraphQL/GraphQL.php
index 1cb78ec..6ab2a08 100644
--- a/src/GraphQL/GraphQL.php
+++ b/src/GraphQL/GraphQL.php
@@ -4,10 +4,16 @@
namespace Luracast\Restler\GraphQL;
use Exception;
+use GraphQL\Error\DebugFlag;
+use GraphQL\Server\ServerConfig;
+use GraphQL\Server\StandardServer;
+use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use Illuminate\Support\Str;
+use Luracast\Restler\Contracts\AccessControlInterface;
+use Luracast\Restler\Contracts\AuthenticationInterface;
+use Luracast\Restler\Contracts\DependentTrait;
use Luracast\Restler\Data\Route;
use Luracast\Restler\Defaults;
use Luracast\Restler\Exceptions\HttpException;
@@ -17,11 +23,10 @@
use Luracast\Restler\Utils\ClassName;
use Luracast\Restler\Utils\CommentParser;
use Luracast\Restler\Utils\PassThrough;
-use Math;
-use ratelimited\Authors;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
use ReflectionClass;
use ReflectionMethod;
-use Say;
use Throwable;
/**
@@ -29,15 +34,38 @@
*/
class GraphQL
{
+ use DependentTrait;
+
const UI_GRAPHQL_PLAYGROUND = 'graphql-playground';
const UI_GRAPHIQL = 'graphiql';
-
+ public const INVALID_TYPES = ['mixed', 'array'];
public static $UI = self::UI_GRAPHQL_PLAYGROUND;
+ public static $serverConfig = [
+ 'rootValue' => ['prefix' => 'You said: '],
+ 'queryBatching' => true,
+ 'debugFlag' => DebugFlag::NONE,
+ ];
+
public static $context = [];
public static $definitions = [];
public static $mutations = [];
public static $queries = [];
+ public static $showDescriptions = false;
+ /**
+ * Access Control - uses Defaults::$apiAccessLevel when set to null
+ *
+ * @var int|null set the default api access mode
+ * value of 0 = public api
+ * value of 1 = hybrid api using `@access hybrid` comment
+ * value of 2 = protected api using `@access protected` comment
+ * value of 3 = protected api using `protected function` method
+ */
+ public static $apiAccessLevel = null;
+ /**
+ * @var array
+ */
+ private static $authClasses;
/**
* @var Restler
*/
@@ -46,46 +74,45 @@ class GraphQL
* @var StaticProperties
*/
private $graphQL;
+ /**
+ * @var ServerRequestInterface
+ */
+ private $request;
- public function __construct(Restler $restler, StaticProperties $graphQL)
+ public function __construct(Restler $restler, StaticProperties $graphQL, ServerRequestInterface $request)
{
$this->restler = $restler;
+ $graphQL->context['restler'] = $restler;
$graphQL->context['maker'] = [$restler, 'make'];
+ $this->request = $graphQL->context['request'] = $request;
$this->graphQL = $graphQL;
}
/**
- * loads graphql client
- * @return \Psr\Http\Message\ResponseInterface
- * @throws HttpException
- */
- public function get()
- {
- return PassThrough::file(__DIR__ . '/client/' . static::$UI . '.html');
- }
-
-
- /**
- * runs graphql queries
- * @param string $query {@from body}
- * @param array $variables {@from body}
+ * protected methods will need at least one authentication class to be set
+ * in order to allow that method to be executed
*
- * @return array|mixed[]
+ * @param string $className of the authentication class
+ * @throws Exception
*/
- public function post(string $query = '', array $variables = [])
+ public static function addAuthenticator(string $className): void
{
- $queryType = new ObjectType(['name' => 'Query', 'fields' => static::$queries]);
- $mutationType = new ObjectType(['name' => 'Mutation', 'fields' => static::$mutations]);
- $schema = new Schema(['query' => $queryType, 'mutation' => $mutationType]);
- $root = ['prefix' => 'You said: '];
- try {
- $result = \GraphQL\GraphQL::executeQuery($schema, $query, $root, $this->graphQL->context, $variables);
- return $result->toArray();
- } catch (Exception $exception) {
- return [
- 'errors' => [['message' => $exception->getMessage()]]
- ];
+ $implements = class_implements($className);
+ if (!isset($implements[AuthenticationInterface::class])) {
+ throw new Exception(
+ $className .
+ ' is an invalid authenticator class; it must implement ' .
+ 'AuthenticationInterface.'
+ );
+ }
+ if (!in_array($className, Defaults::$implementations[AuthenticationInterface::class])) {
+ Defaults::$implementations[AuthenticationInterface::class][] = $className;
}
+ if (isset($implements[AccessControlInterface::class]) &&
+ !in_array($className, Defaults::$implementations[AccessControlInterface::class])) {
+ Defaults::$implementations[AccessControlInterface::class][] = $className;
+ }
+ static::$authClasses[] = $className;
}
/**
@@ -94,6 +121,7 @@ public function post(string $query = '', array $variables = [])
*/
public static function mapApiClasses(array $map): void
{
+ static::checkDependencies();
try {
foreach ($map as $className => $name) {
if (is_numeric($className)) {
@@ -117,9 +145,8 @@ public static function mapApiClasses(array $map): void
if ($method->isStatic()) {
continue;
}
- $methodName = strtolower($method->getName());
//method name should not begin with _
- if ($methodName[0] == '_') {
+ if ($method->getName()[0] == '_') {
continue;
}
$metadata = [];
@@ -140,43 +167,53 @@ public static function mapApiClasses(array $map): void
if (is_null($scope)) {
$scope = Router::scope($class);
}
- static::addMethod($name, $method, $metadata, $scope);
+ static::addMethod($method, $name, $metadata, $scope);
}
}
} catch (Throwable $e) {
- throw new Exception(
- "mapAPIClasses failed. " . $e->getMessage(),
+ throw new HttpException(
$e->getCode(),
+ "Failed to map `$className` class to GraphQL. " . $e->getMessage(),
+ [],
$e
);
}
}
- public static function addMethod(string $name, ReflectionMethod $method, ?array $metadata = null, array $scope = [])
- {
+ public static function addMethod(
+ ReflectionMethod $method,
+ string $baseName = '',
+ ?array $metadata = null,
+ array $scope = []
+ ) {
$route = Route::fromMethod($method, $metadata, $scope);
+ $route->authClasses = static::$authClasses;
if ($mutation = $route->mutation ?? false) {
return static::addRoute($mutation, $route, true);
}
if ($query = $route->query ?? false) {
return static::addRoute($query, $route, false);
}
- $single = Str::singular($name);
- switch ($route->httpMethod) {
- case 'POST':
- $name = 'make' . $single;
- break;
- case 'DELETE':
- $name = 'remove' . $single;
- break;
- case 'PUT':
- case 'PATCH':
- $name = 'update' . $single;
- break;
- default:
- $name = isset($route->parameters['id'])
- ? 'get' . $single
- : lcfirst($name);
+ if (!empty($route->url)) {
+ $name = empty($baseName) ? lcfirst($route->url) : lcfirst($baseName) . ucfirst($route->url);
+ } else {
+ $single = empty($baseName) ? '' : Str::singular($baseName);
+ switch ($route->httpMethod) {
+ case 'POST':
+ $name = 'make' . $single;
+ break;
+ case 'DELETE':
+ $name = 'remove' . $single;
+ break;
+ case 'PUT':
+ case 'PATCH':
+ $name = 'update' . $single;
+ break;
+ default:
+ $name = isset($route->parameters['id'])
+ ? 'get' . $single
+ : lcfirst($baseName);
+ }
}
return static::addRoute($name, $route, 'GET' !== $route->httpMethod);
}
@@ -186,4 +223,80 @@ public static function addRoute(string $name, Route $route, bool $isMutation = f
$target = $isMutation ? 'mutations' : 'queries';
static::$$target[$name] = $route->toGraphQL();
}
+
+ /**
+ * @return array {@type associative}
+ * CLASS_NAME => vendor/project:version
+ */
+ public static function dependencies(): array
+ {
+ return ['GraphQL\Type\Definition\Type' => 'webonyx/graphql-php'];
+ }
+
+ /**
+ * Creates enum and makes sure is name is unique to avoid conflicts
+ * @param array $config
+ * @return EnumType
+ */
+ public static function enum(array $config): EnumType
+ {
+ $name = $config['name'] ?? 'enum';
+ $number = 1;
+ while (isset(self::$definitions[$name])) {
+ $config['name'] = $name = $name . (++$number);
+ }
+ self::$definitions[$name] = new EnumType($config);
+ return self::$definitions[$name];
+ }
+
+ /**
+ * loads graphql client
+ * @param string $query
+ * @param array $variables
+ * @return ResponseInterface
+ * @throws HttpException
+ */
+ public function get(string $query = '', array $variables = [])
+ {
+ if (!empty($query)) {
+ return $this->handle();
+ }
+ return PassThrough::file(__DIR__ . '/client/' . static::$UI . '.html');
+ }
+
+ private function handle()
+ {
+ try {
+ $data = [];
+ $data['query'] = new ObjectType(['name' => 'Query', 'fields' => static::$queries]);
+ if (!empty(self::$mutations)) {
+ $data['mutation'] = new ObjectType(['name' => 'Mutation', 'fields' => static::$mutations]);
+ }
+ $schema = new Schema($data);
+ $config = ServerConfig::create(
+ static::$serverConfig +
+ ['context' => $this->graphQL->context, 'schema' => $schema]
+ );
+ $server = new StandardServer($config);
+ return $server->executePsrRequest(
+ $this->request->withParsedBody(json_decode((string)$this->request->getBody(), true))
+ );
+ } catch (Exception $exception) {
+ return [
+ 'errors' => [['message' => $exception->getMessage()]]
+ ];
+ }
+ }
+
+ /**
+ * runs graphql queries
+ * @param string $query {@from body}
+ * @param array $variables {@from body}
+ *
+ * @return array|mixed[]
+ */
+ public function post(string $query = '', array $variables = [])
+ {
+ return $this->handle();
+ }
}
diff --git a/src/MediaTypes/Amf.php b/src/MediaTypes/Amf.php
index bb50cc8..810b57d 100644
--- a/src/MediaTypes/Amf.php
+++ b/src/MediaTypes/Amf.php
@@ -15,10 +15,10 @@ class Amf extends Dependent implements RequestMediaTypeInterface, ResponseMediaT
* @return array {@type associative}
* CLASS_NAME => vendor/project:version
*/
- public function dependencies()
+ public static function dependencies(): array
{
return [
- 'ZendAmf\Parser\Amf3\Deserializer' => 'zendframework/zendamf:dev-master',
+ 'ZendAmf\Parser\Amf3\Deserializer' => 'zendframework/zendamf',
];
}
@@ -32,7 +32,7 @@ public function encode($data, ResponseHeaders $responseHeaders, bool $humanReada
return $stream->getStream();
}
- public function decode($data)
+ public function decode(string $data)
{
$stream = new InputStream(substr($data, 1));
$deserializer = new Deserializer($stream);
diff --git a/src/MediaTypes/Dependent.php b/src/MediaTypes/Dependent.php
index 2d4110c..4d91a31 100644
--- a/src/MediaTypes/Dependent.php
+++ b/src/MediaTypes/Dependent.php
@@ -1,32 +1,15 @@
vendor/project:version
- */
- abstract public function dependencies();
-
- protected function checkDependencies()
- {
- foreach ($this->dependencies() as $className => $package) {
- if (!class_exists($className, true)) {
- throw new HttpException(
- 500,
- get_called_class() . ' has external dependency. Please run `composer require ' .
- $package . '` from the project root. Read https://getcomposer.org for more info'
- );
- }
- }
- }
+ use DependentTrait;
public function __construct(Convert $convert)
{
parent::__construct($convert);
- $this->checkDependencies();
+ static::checkDependencies();
}
-}
\ No newline at end of file
+}
diff --git a/src/MediaTypes/Html.php b/src/MediaTypes/Html.php
index bfc139a..80c2abc 100644
--- a/src/MediaTypes/Html.php
+++ b/src/MediaTypes/Html.php
@@ -18,6 +18,7 @@
use Luracast\Restler\Contracts\ContainerInterface;
use Luracast\Restler\Contracts\ResponseMediaTypeInterface;
use Luracast\Restler\Contracts\SessionInterface;
+use Luracast\Restler\Data\Route;
use Luracast\Restler\Defaults;
use Luracast\Restler\Exceptions\HttpException;
use Luracast\Restler\ResponseHeaders;
@@ -26,6 +27,8 @@
use Luracast\Restler\UI\Forms;
use Luracast\Restler\UI\Nav;
use Luracast\Restler\Utils\Convert;
+use Luracast\Restler\Utils\Text;
+use Psr\Http\Message\ServerRequestInterface;
use Throwable;
use Twig\Environment;
use Twig\Extension\DebugExtension;
@@ -57,7 +60,7 @@ class Html extends MediaType implements ResponseMediaTypeInterface
* @var array global key value pair to be supplied to the templates. All
* keys added here will be available as a variable inside the template
*/
- public static $data = array();
+ public static $data = [];
/**
* @var string set it to the location of your the view files. Defaults to
* views folder which is same level as vendor directory.
@@ -66,7 +69,7 @@ class Html extends MediaType implements ResponseMediaTypeInterface
/**
* @var array template and its custom extension key value pair
*/
- public static $customTemplateExtensions = array('blade' => 'blade.php');
+ public static $customTemplateExtensions = ['blade' => 'blade.php'];
/**
* @var bool used internally for error handling
*/
@@ -92,11 +95,21 @@ class Html extends MediaType implements ResponseMediaTypeInterface
* @var SessionInterface
*/
private $session;
+ /**
+ * @var ServerRequestInterface
+ */
+ private $request;
+ /**
+ * @var Route
+ */
+ private $route;
public function __construct(
Restler $restler,
+ Route $route,
SessionInterface $session,
ContainerInterface $container,
+ ServerRequestInterface $request,
StaticProperties $html,
StaticProperties $defaults,
Convert $convert
@@ -117,39 +130,12 @@ public function __construct(
}
}
$this->restler = $restler;
+ $this->route = $route;
$this->session = $session;
$this->container = $container;
$this->html = $html;
$this->defaults = $defaults;
- }
-
- public function guessViewName($path)
- {
- if (empty($path)) {
- $path = 'index';
- } elseif (strpos($path, '/')) {
- $path .= '/index';
- }
- $file = $this->html['viewPath'] . '/' . $path . '.' . $this->getViewExtension();
- $this->html->data['guessedView'] = $file;
- return $this->html['useSmartViews'] && is_readable($file)
- ? $path
- : $this->html->errorView;
- }
-
- public function getViewExtension()
- {
- return $this->html['customTemplateExtensions'][$this->html['template']] ?? $this->html['template'];
- }
-
- public function getViewFile($fullPath = false, $includeExtension = true): string
- {
- $v = $fullPath ? $this->html->viewPath . '/' : '';
- $v .= $this->html->view;
- if ($includeExtension) {
- $v .= '.' . $this->getViewExtension();
- }
- return $v;
+ $this->request = $request;
}
public function encode($data, ResponseHeaders $responseHeaders, bool $humanReadable = false)
@@ -171,14 +157,21 @@ public function encode($data, ResponseHeaders $responseHeaders, bool $humanReada
'success' => $success,
'error' => $error,
'restler' => $this->restler,
- 'container' => $this->container
+ 'container' => $this->container,
+ 'baseUrl' => $this->restler->baseUrl,
+ 'currentPath' => $this->restler->path,
]
);
- $data->baseUrl = $this->restler->baseUrl;
+ $rpath = $this->request->getUri()->getPath();
+ $data->resourcePathNormalizer = (!empty($data->currentPath) && Text::endsWith($rpath, '/')) ||
+ ('index' !== $data->currentPath && Text::endsWith($rpath, 'index.html'))
+ ? '../' : './';
$data->basePath = $data->baseUrl->getPath();
- $data->currentPath = $this->restler->path;
- $api = $data->api = $this->restler->route;
- $metadata = $api;
+ $data->path = '/' . trim(
+ str_replace('//', '/', $data->basePath . $this->route->resource['path']), '/'
+ );
+ //$data->path = '/' . ltrim(explode('index', $data->basePath . $data->currentPath)[0],'/');
+ $metadata = $data->api = $this->restler->route;
$view = $success ? 'view' : 'errorView';
$value = false;
if ($this->parseViewMetadata && isset($metadata->{$view})) {
@@ -241,6 +234,25 @@ public function encode($data, ResponseHeaders $responseHeaders, bool $humanReada
}
}
+ public function guessViewName($path)
+ {
+ if (empty($path)) {
+ $path = 'index';
+ } elseif (strpos($path, '/')) {
+ $path .= '/index';
+ }
+ $file = $this->html['viewPath'] . '/' . $path . '.' . $this->getViewExtension();
+ $this->html->data['guessedView'] = $file;
+ return $this->html['useSmartViews'] && is_readable($file)
+ ? $path
+ : $this->html->errorView;
+ }
+
+ public function getViewExtension()
+ {
+ return $this->html['customTemplateExtensions'][$this->html['template']] ?? $this->html['template'];
+ }
+
private function reset()
{
$this->html->view = 'debug';
@@ -339,6 +351,16 @@ public function php(ArrayObject $data, $debug = true)
return is_string($value) ? $value : '';
}
+ public function getViewFile($fullPath = false, $includeExtension = true): string
+ {
+ $v = $fullPath ? $this->html->viewPath . '/' : '';
+ $v .= $this->html->view;
+ if ($includeExtension) {
+ $v .= '.' . $this->getViewExtension();
+ }
+ return $v;
+ }
+
/**
* @param ArrayObject $data
* @param bool $debug
@@ -349,11 +371,11 @@ public function twig(ArrayObject $data, $debug = true)
{
$loader = new FilesystemLoader($this->html->viewPath);
$twig = new Environment(
- $loader, array(
- 'cache' => static::$cacheDirectory ?? false,
- 'debug' => $debug,
- 'use_strict_variables' => $debug,
- )
+ $loader, [
+ 'cache' => static::$cacheDirectory ?? false,
+ 'debug' => $debug,
+ 'use_strict_variables' => $debug,
+ ]
);
if ($debug) {
$twig->addExtension(new DebugExtension());
@@ -363,7 +385,7 @@ public function twig(ArrayObject $data, $debug = true)
new TwigFunction(
'form',
'Luracast\Restler\UI\Forms::get',
- array('is_safe' => array('html'))
+ ['is_safe' => ['html']]
)
);
$twig->addFunction(
@@ -408,12 +430,12 @@ public function mustache(ArrayObject $data, $debug = true)
if (!isset($data['nav'])) {
//$data['nav'] = array_values(Nav::get()); //TODO get nav to work
}
- $options = array(
+ $options = [
'loader' => new \Mustache_Loader_FilesystemLoader(
$this->html->viewPath,
- array('extension' => $this->getViewExtension())
+ ['extension' => $this->getViewExtension()]
),
- 'helpers' => array(
+ 'helpers' => [
'form' => function ($text, \Mustache_LambdaHelper $m) {
$params = explode(',', $m->render($text));
return call_user_func_array(
@@ -421,8 +443,8 @@ public function mustache(ArrayObject $data, $debug = true)
$params
);
},
- )
- );
+ ]
+ ];
if (!$debug) {
$options['cache'] = $this->html->cacheDirectory;
}
@@ -442,7 +464,7 @@ function () use ($engine) {
return $engine;
}
);
- $phpEngine = new PhpEngine();
+ $phpEngine = new PhpEngine($filesystem);
$resolver->register(
'php',
function () use ($phpEngine) {
diff --git a/src/MediaTypes/Plist.php b/src/MediaTypes/Plist.php
index 2f2264e..474d2a8 100644
--- a/src/MediaTypes/Plist.php
+++ b/src/MediaTypes/Plist.php
@@ -1,13 +1,11 @@
vendor/project:version
*/
- public function dependencies()
+ public static function dependencies()
{
return [
'CFPropertyList\CFPropertyList' => 'rodneyrehm/plist:dev-master'
diff --git a/src/MediaTypes/Spreadsheet.php b/src/MediaTypes/Spreadsheet.php
index 365ce60..fdc0a80 100644
--- a/src/MediaTypes/Spreadsheet.php
+++ b/src/MediaTypes/Spreadsheet.php
@@ -8,11 +8,10 @@
use Box\Spout\Writer\Common\Creator\Style\StyleBuilder;
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
use Luracast\Restler\Contracts\DownloadableFileMediaTypeInterface;
-use Luracast\Restler\Contracts\ResponseMediaTypeInterface;
+use Luracast\Restler\Defaults;
use Luracast\Restler\Exceptions\HttpException;
use Luracast\Restler\ResponseHeaders;
use Luracast\Restler\Utils\Convert;
-use Luracast\Restler\Defaults;
class Spreadsheet extends Dependent implements DownloadableFileMediaTypeInterface
@@ -20,22 +19,22 @@ class Spreadsheet extends Dependent implements DownloadableFileMediaTypeInterfac
const MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
const EXTENSION = 'xlsx';
+ public function __construct(Convert $convert)
+ {
+ parent::__construct($convert);
+ }
+
/**
* @return array {@type associative}
* CLASS_NAME => vendor/project:version
*/
- public function dependencies()
+ public static function dependencies(): array
{
return [
'Box\Spout\Common\Entity\Row' => 'box/spout:dev-master'
];
}
- public function __construct(Convert $convert)
- {
- parent::__construct($convert);
- }
-
/**
* @inheritDoc
*/
diff --git a/src/MediaTypes/UrlEncoded.php b/src/MediaTypes/UrlEncoded.php
index 65e7d7a..98fe55a 100644
--- a/src/MediaTypes/UrlEncoded.php
+++ b/src/MediaTypes/UrlEncoded.php
@@ -58,8 +58,6 @@ public static function decoderTypeFix(array $data)
$data[$k] = $v = $v === 'true';
} elseif (is_array($v)) {
$data[$k] = $v = static::decoderTypeFix($v);
- } elseif (empty($v) && $v != 0) {
- unset($data[$k]);
}
}
return $data;
diff --git a/src/MediaTypes/Yaml.php b/src/MediaTypes/Yaml.php
index 2282f2c..424b361 100644
--- a/src/MediaTypes/Yaml.php
+++ b/src/MediaTypes/Yaml.php
@@ -4,7 +4,6 @@
use Luracast\Restler\Contracts\RequestMediaTypeInterface;
use Luracast\Restler\Contracts\ResponseMediaTypeInterface;
use Luracast\Restler\ResponseHeaders;
-use Luracast\Restler\Utils\Convert;
use Symfony\Component\Yaml\Yaml as Y;
class Yaml extends Dependent implements RequestMediaTypeInterface, ResponseMediaTypeInterface
@@ -16,9 +15,9 @@ class Yaml extends Dependent implements RequestMediaTypeInterface, ResponseMedia
* @return array {@type associative}
* CLASS_NAME => vendor/project:version
*/
- public function dependencies()
+ public static function dependencies()
{
- return ['Symfony\Component\Yaml\Yaml' => 'symfony/yaml:*'];
+ return ['Symfony\Component\Yaml\Yaml' => 'symfony/yaml'];
}
public function decode(string $data)
diff --git a/src/OpenApi3/Explorer.php b/src/OpenApi3/Explorer.php
index 1304b7d..dad6661 100644
--- a/src/OpenApi3/Explorer.php
+++ b/src/OpenApi3/Explorer.php
@@ -6,7 +6,8 @@
ComposerInterface,
DownloadableFileMediaTypeInterface,
ExplorableAuthenticationInterface,
- ProvidesMultiVersionApiInterface};
+ ProvidesMultiVersionApiInterface
+};
use Luracast\Restler\Core;
use Luracast\Restler\Data\{Param, Returns, Route, Type};
use Luracast\Restler\Defaults;
@@ -15,11 +16,12 @@
use Luracast\Restler\OpenApi3\Tags\TagByBasePath;
use Luracast\Restler\OpenApi3\Tags\Tagger;
use Luracast\Restler\Router;
-use Luracast\Restler\Utils\{ClassName, Convert, PassThrough, Text, Type as TypeUtil};
+use Luracast\Restler\Utils\{ClassName, PassThrough, Text, Type as TypeUtil};
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use ReflectionClass;
+use ReflectionException;
use stdClass;
class Explorer implements ProvidesMultiVersionApiInterface
@@ -31,6 +33,20 @@ class Explorer implements ProvidesMultiVersionApiInterface
public static $hideProtected = false;
public static $allowScalarValueOnRequestBody = false;
public static $servers = [];
+ /**
+ * @var array
+ * @link https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration
+ */
+ public static $uiConfig = [
+ 'deepLinking' => false,
+ 'displayOperationId' => false,
+ 'syntaxHighlight' => [
+ 'theme' => 'tomorrow-night', //agate or arta or monokai or nord" or obsidian or tomorrow-night
+ ],
+ 'filter' => true, //null or a string to filter by
+ 'validatorUrl' => null //disables validation change to "https://validator.swagger.io/validator" to enable
+ ];
+
/**
* @var array mapping PHP types to JS
*/
@@ -107,11 +123,11 @@ public static function setTagger(Tagger $tagger): void
* Serve static files for explorer
* @throws HttpException
*/
- public function index()
+ public function index(): ResponseInterface
{
- $path = $this->request->getUri()->getPath();
+ $path = $this->request->getUri()->withQuery('')->getPath();
if (!empty($path) && !Text::endsWith($path, '/')) {
- throw new Redirect((string)$this->request->getUri() . '/');
+ throw new Redirect((string)$this->request->getUri()->withPath($path . '/'));
}
return $this->get('index.html');
}
@@ -123,7 +139,7 @@ public function index()
*
* @url GET {filename}
*/
- public function get($filename)
+ public function get($filename): ResponseInterface
{
$filename = str_replace(['../', './', '\\', '..', '.php'], '', $filename);
if (empty($filename)) {
@@ -135,10 +151,15 @@ public function get($filename)
return PassThrough::file($file, $this->request->getHeaderLine('If-Modified-Since'));
}
+ public function config()
+ {
+ return (object)static::$uiConfig;
+ }
+
/**
- * @return stdClass
+ * @return object
*/
- public function docs()
+ public function docs(): stdClass
{
$s = new stdClass();
$s->openapi = static::OPEN_API_SPEC_VERSION;
@@ -173,7 +194,7 @@ public function docs()
return $s;
}
- private function info(int $version)
+ private function info(int $version): array
{
$info = array_filter(call_user_func(static::$infoClass . '::format', static::OPEN_API_SPEC_VERSION));
$info['description'] .= 'Api Documentation - [ReDoc](' . dirname(
@@ -186,7 +207,7 @@ private function info(int $version)
/**
* @return array
*/
- private function servers()
+ private function servers(): array
{
return empty(static::$servers)
? [
@@ -202,12 +223,12 @@ private function servers()
* @param int $version
* @return array
*/
- private function paths(int $version = 1)
+ private function paths(int $version = 1): array
{
$self = explode('/', $this->route->path);
array_pop($self);
$self = implode('/', $self);
- $selfExclude = empty($self) ? ['', '{s0}', 'docs'] : [$self];
+ $selfExclude = empty($self) ? ['', '{s0}', 'docs', 'config'] : [$self];
$map = Router::findAll(
$this->request,
[$this->restler, 'make'],
@@ -233,7 +254,7 @@ private function paths(int $version = 1)
return $paths;
}
- private function operation(Route $route, int $version)
+ private function operation(Route $route, int $version): stdClass
{
$r = new stdClass();
$r->operationId = $this->operationId($route, $version);
@@ -286,7 +307,7 @@ private function operationId(Route $route, int $version, bool $asClassName = fal
return $hash[$id][$asClassName];
}
- private function parameters(Route $route, int $version)
+ private function parameters(Route $route, int $version): array
{
$parameters = $route->filterParams(false);
$body = $route->filterParams(true);
@@ -324,6 +345,25 @@ private function parameters(Route $route, int $version)
return [$r, $requestBody];
}
+ private function parameter(Param $param, $description = '')
+ {
+ $p = (object)[
+ 'name' => $param->name ?? '',
+ 'in' => $param->from,
+ 'description' => $description,
+ 'required' => $param->required,
+ 'schema' => new stdClass(),
+ ];
+
+ $this->setProperties($param, $p->schema);
+
+ if (isset($param->rules['example'])) {
+ $p->examples = [1 => ['value' => $param->rules['example']]];
+ }
+
+ return $p;
+ }
+
private function setProperties(Type $param, stdClass $schema)
{
//primitives
@@ -341,6 +381,7 @@ private function setProperties(Type $param, stdClass $schema)
$schema->type = 'object';
} else { //'indexed == $param->format
$schema->type = 'array';
+ $schema->items = new stdClass;
}
} else {
$target = $schema;
@@ -349,6 +390,11 @@ private function setProperties(Type $param, stdClass $schema)
$schema->items = new stdClass;
$target = $schema->items;
}
+ if ($param->type === UploadedFileInterface::class) {
+ $target->type = 'string';
+ $target->format = 'binary';
+ return;
+ }
$target->type = 'object';
if (!empty($param->properties)) {
$target->properties = new stdClass;
@@ -360,25 +406,6 @@ private function setProperties(Type $param, stdClass $schema)
}
}
- private function parameter(Param $param, $description = '')
- {
- $p = (object)[
- 'name' => $param->name ?? '',
- 'in' => $param->from,
- 'description' => $description,
- 'required' => $param->required,
- 'schema' => new stdClass(),
- ];
-
- $this->setProperties($param, $p->schema);
-
- if (isset($param->rules['example'])) {
- $p->examples = [1 => ['value' => $param->rules['example']]];
- }
-
- return $p;
- }
-
private function scalarProperties(stdClass $s, Type $param)
{
if ($t = static::$dataTypeAlias[$param->type] ?? null) {
@@ -405,8 +432,8 @@ private function scalarProperties(stdClass $s, Type $param)
if (!$param instanceof Param) {
return;
}
- if ($param->default) {
- $s->default = $param->default;
+ if ($param->default[0]) {
+ $s->default = $param->default[1];
}
if ($param->choice) {
$s->enum = $param->choice;
@@ -430,12 +457,18 @@ private function requestBody(Route $route, Param $param, $description = '')
return (object)['$ref' => "#/components/requestBodies/{$param->type}"];
}
- private function modelName(Route $route, int $version)
+ private function modelName(Route $route, int $version): string
{
return ucfirst($this->operationId($route, $version, true)) . 'Model';
}
- private function responses(Route $route)
+ /**
+ * @param Route $route
+ * @return array[]
+ * @throws HttpException
+ * @throws ReflectionException
+ */
+ private function responses(Route $route): array
{
$code = '200';
if (isset($route->status)) {
@@ -462,6 +495,7 @@ private function responses(Route $route)
}
if (is_array($throws = $route->throws ?? null)) {
+ /** @var ComposerInterface $composer */
$composer = ClassName::get(ComposerInterface::class);
foreach ($throws as $throw) {
$r[$throw['code']] = ['description' => $throw['message']];
diff --git a/src/OpenApi3/Security/BearerAuth.php b/src/OpenApi3/Security/BearerAuth.php
index 4ce260f..ea2ed25 100644
--- a/src/OpenApi3/Security/BearerAuth.php
+++ b/src/OpenApi3/Security/BearerAuth.php
@@ -18,4 +18,4 @@ public function __construct(string $bearerFormat, string $description = '')
$this->bearerFormat = $bearerFormat;
$this->description = $description;
}
-}
\ No newline at end of file
+}
diff --git a/src/OpenApi3/client/index.html b/src/OpenApi3/client/index.html
index 2a18946..975eebd 100644
--- a/src/OpenApi3/client/index.html
+++ b/src/OpenApi3/client/index.html
@@ -4,9 +4,9 @@
Api Explorer
-
-
-
+
+
+
",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:"