commit
548d376e96
5 changed files with 302 additions and 0 deletions
-
0README
-
29composer.json
-
173lib/NetteUtils/Rest/HttpServicePresenter.php
-
46lib/NetteUtils/Routing/RestRoute.php
-
54lib/NetteUtils/System/Bootstrap.php
@ -0,0 +1,29 @@ |
|||||
|
{ |
||||
|
"name": "pavlicek.dev/webapp-nette-utils", |
||||
|
"description": "Collection of utilities, tools and code snippets useful when developing Nette based web application, particularily with REST APIs", |
||||
|
"type": "project", |
||||
|
"license": ["Proprietary at this time"], |
||||
|
"authors": [ |
||||
|
{ |
||||
|
"name": "Jan Pavlíček", |
||||
|
"email": "jan@pavlicek.dev" |
||||
|
} |
||||
|
], |
||||
|
"config": { |
||||
|
"optimize-autoloader": true, |
||||
|
"platform": { |
||||
|
"php": "8.2" |
||||
|
} |
||||
|
}, |
||||
|
"require": { |
||||
|
"php": ">= 8.2.0", |
||||
|
"jms/serializer": "*", |
||||
|
"latte/latte": "*", |
||||
|
"nette/application": "*", |
||||
|
"nette/http": "*", |
||||
|
"nette/security": "*" |
||||
|
}, |
||||
|
"autoload": { |
||||
|
"psr-0": {"": "lib/"} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,173 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace NetteUtils\Rest; |
||||
|
|
||||
|
use Throwable; |
||||
|
|
||||
|
use Nette\Application\UI\Presenter; |
||||
|
use Nette\Http\IResponse; |
||||
|
use Nette\Http\IRequest; |
||||
|
use Nette\Application\Responses\TextResponse; |
||||
|
use Nette\Application\Responses\JsonResponse; |
||||
|
use JMS\Serializer\Serializer; |
||||
|
|
||||
|
/** |
||||
|
* Ancestor for all presenters that are intended to offer REST API. Provides utility methods |
||||
|
* to simplify response sending as well as some basic dependencies. Also provides authentication. |
||||
|
* |
||||
|
* @author Jan Pavlíček <jan@pavlicek.dev> |
||||
|
* @since 1.0.0 |
||||
|
*/ |
||||
|
abstract class HttpServicePresenter extends Presenter |
||||
|
{ |
||||
|
protected IResponse $response; |
||||
|
|
||||
|
|
||||
|
protected IRequest $request; |
||||
|
|
||||
|
|
||||
|
protected Serializer $serializer; |
||||
|
|
||||
|
|
||||
|
|
||||
|
public function injectResponse(IResponse $response) : void |
||||
|
{ |
||||
|
$this->response = $response; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
public function injectRequest(IRequest $request) : void |
||||
|
{ |
||||
|
$this->request = $request; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
public function injectSerializer(Serializer $serializer) : void |
||||
|
{ |
||||
|
$this->serializer = $serializer; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* Set some basic CORS headers and proper configuration of OPTIONS methods. Then authenticate each request. |
||||
|
*/ |
||||
|
protected function startup() |
||||
|
{ |
||||
|
parent::startup(); |
||||
|
$this->response->setContentType('application/json', 'utf-8'); |
||||
|
$this->response->setHeader('Access-Control-Allow-Origin', '*'); |
||||
|
$this->response->setHeader('Access-Control-Expose-Headers', 'id'); |
||||
|
if($this->request->method == 'OPTIONS') { |
||||
|
$this->response->setHeader('Access-Control-Allow-Methods', 'POST, PUT, GET, DELETE'); |
||||
|
$this->response->setHeader( |
||||
|
'Access-Control-Allow-Headers', 'origin, content-type, accept, X-API-Key'); |
||||
|
$this->response->setCode(IResponse::S200_OK); |
||||
|
$this->terminate(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* Returns OpenAPI specification in yml format |
||||
|
*/ |
||||
|
public function actionOpenapi() |
||||
|
{ |
||||
|
$spec = @$this->getContext()->getParameters()['docDir'] . "/openapi.yml"; |
||||
|
$this->sendResponse(new \Nette\Application\Responses\FileResponse($spec, 'openapi.yml', 'application/x-yaml')); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
protected function sendValidationErrorResponse($message = null, $code = IResponse::S400_BAD_REQUEST) |
||||
|
{ |
||||
|
$this->response->setCode($code); |
||||
|
$this->sendResponse(new TextResponse($this->ensureJsonAsString($message))); |
||||
|
$this->terminate(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
protected function sendExceptionErrorResponse(Throwable $exception, $code = IResponse::S500_INTERNAL_SERVER_ERROR) |
||||
|
{ |
||||
|
$this->response->setCode(IResponse::S500_INTERNAL_SERVER_ERROR); |
||||
|
$this->sendResponse(new JsonResponse(['fault' => [ |
||||
|
'faultcode' => 500, |
||||
|
'faultstring' => $exception->getMessage(), |
||||
|
'detail' => get_class($exception) . ": " . $exception->getMessage() |
||||
|
]])); |
||||
|
$this->terminate(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
protected function sendJsonResponse($json, $code = IResponse::S200_OK) |
||||
|
{ |
||||
|
$this->response->setCode($code); |
||||
|
$this->sendResponse(new TextResponse($this->ensureJsonAsString($json))); |
||||
|
$this->terminate(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
protected function sendCreatedResponse($json = null, $code = IResponse::S201_CREATED) |
||||
|
{ |
||||
|
$this->sendResponseWithCodeAndOptionalJson($code, $json); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
protected function sendAcceptedResponse($json = null, $code = IResponse::S202_ACCEPTED) |
||||
|
{ |
||||
|
$this->sendResponseWithCodeAndOptionalJson($code, $json); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
protected function sendNotFoundResponse($json = null, $code = IResponse::S404_NOT_FOUND) |
||||
|
{ |
||||
|
$this->sendResponseWithCodeAndOptionalJson($code, $json); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
protected function sendResponseWithCodeAndOptionalJson(int $code, $json = null) |
||||
|
{ |
||||
|
$this->response->setCode($code); |
||||
|
if ($json) { |
||||
|
$this->sendResponse(new TextResponse($this->ensureJsonAsString($json))); |
||||
|
} |
||||
|
$this->terminate(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
protected function parseJsonPayload(bool $exception_on_failure = false, bool $as_array = true) : array |
||||
|
{ |
||||
|
$payload = json_decode(file_get_contents('php://input'), $as_array); |
||||
|
|
||||
|
if (!$payload) { |
||||
|
if ($exception_on_failure) { |
||||
|
throw new \RuntimeException("No valid json to deserialize in request body"); |
||||
|
} else { |
||||
|
$this->sendValidationErrorResponse("Malformed JSON body"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $payload; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
private function ensureJsonAsString($json) : string |
||||
|
{ |
||||
|
if (is_array($json)) { |
||||
|
$json = json_encode($json); |
||||
|
} elseif (is_object($json)) { |
||||
|
$json = $this->serializer->serialize($json, 'json'); |
||||
|
} |
||||
|
return $json; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace NetteUtils\Routing; |
||||
|
|
||||
|
use Nette\Application\Routers\Route; |
||||
|
use Nette\Http\IRequest; |
||||
|
|
||||
|
/** |
||||
|
* Extended Nette Route with capability to validate request method |
||||
|
* |
||||
|
* @author Jan Pavlíček <jan@pavlicek.dev> |
||||
|
*/ |
||||
|
class RestRoute extends Route |
||||
|
{ |
||||
|
/** |
||||
|
* List of HTTP methods that will match with this route |
||||
|
* @var array |
||||
|
*/ |
||||
|
protected $allowedMethods = ['GET', 'POST', 'OPTIONS']; |
||||
|
|
||||
|
|
||||
|
public function __construct($methods, $mask, $metadata = [], $flags = 0) |
||||
|
{ |
||||
|
if ($methods) { |
||||
|
$this->allowedMethods = array_merge(explode('|', $methods), ['OPTIONS']); |
||||
|
} |
||||
|
|
||||
|
parent::__construct($mask, $metadata, $flags); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* Maps HTTP request to a Request object. Does not match, if methods are defined and request |
||||
|
* does not match one of them. |
||||
|
* |
||||
|
* @return Nette\Application\Request|NULL |
||||
|
*/ |
||||
|
public function match(IRequest $httpRequest) : ?array |
||||
|
{ |
||||
|
if (!in_array($httpRequest->getMethod(), $this->allowedMethods)) { |
||||
|
return null; |
||||
|
} |
||||
|
return parent::match($httpRequest); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace NetteUtils\System; |
||||
|
|
||||
|
use Nette\Bootstrap\Configurator; |
||||
|
|
||||
|
class Bootstrap |
||||
|
{ |
||||
|
public static function boot(): Configurator |
||||
|
{ |
||||
|
$configurator = new Configurator; |
||||
|
|
||||
|
if (getenv('APP_ENV') == 'local' || getenv('PHP_ENV') == 'development') { |
||||
|
$configurator->setDebugMode(true); |
||||
|
} else { |
||||
|
$configurator->setDebugMode(false); |
||||
|
} |
||||
|
$configurator->enableTracy(__DIR__ . '/../log'); |
||||
|
|
||||
|
self::setupCommon($configurator); |
||||
|
|
||||
|
$configurator->addConfig(__DIR__ . '/Config/local.neon'); |
||||
|
|
||||
|
return $configurator; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
public static function bootForTests(): Configurator |
||||
|
{ |
||||
|
$configurator = new Configurator; |
||||
|
|
||||
|
self::setupCommon($configurator); |
||||
|
|
||||
|
$configurator->addConfig(__DIR__ . '/Config/test.neon'); |
||||
|
|
||||
|
return $configurator; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
protected static function setupCommon(Configurator $configurator) : void |
||||
|
{ |
||||
|
$configurator->setTimeZone('Europe/Prague'); |
||||
|
$configurator->setTempDirectory(__DIR__ . '/../temp'); |
||||
|
|
||||
|
$configurator->createRobotLoader() |
||||
|
->addDirectory(__DIR__) |
||||
|
->addDirectory(__DIR__ . '/../src') |
||||
|
->register(); |
||||
|
|
||||
|
$configurator->addConfig(__DIR__ . '/Config/config.neon'); |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue