commit 548d376e9622766c83dbb7b0576b52a9be106f94 Author: Jan Pavlíček Date: Sun Jan 14 07:40:38 2024 +0100 Initial commit diff --git a/README b/README new file mode 100644 index 0000000..e69de29 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..dede6c6 --- /dev/null +++ b/composer.json @@ -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/"} + } +} diff --git a/lib/NetteUtils/Rest/HttpServicePresenter.php b/lib/NetteUtils/Rest/HttpServicePresenter.php new file mode 100755 index 0000000..3979e55 --- /dev/null +++ b/lib/NetteUtils/Rest/HttpServicePresenter.php @@ -0,0 +1,173 @@ + + * @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; + } +} diff --git a/lib/NetteUtils/Routing/RestRoute.php b/lib/NetteUtils/Routing/RestRoute.php new file mode 100755 index 0000000..7e106f4 --- /dev/null +++ b/lib/NetteUtils/Routing/RestRoute.php @@ -0,0 +1,46 @@ + + */ +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); + } +} diff --git a/lib/NetteUtils/System/Bootstrap.php b/lib/NetteUtils/System/Bootstrap.php new file mode 100644 index 0000000..c6ce11f --- /dev/null +++ b/lib/NetteUtils/System/Bootstrap.php @@ -0,0 +1,54 @@ +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'); + } +}