diff --git a/.github/workflows/build-app-image.yml b/.github/workflows/build-app-image.yml index d77f6a1..4d0c9f3 100644 --- a/.github/workflows/build-app-image.yml +++ b/.github/workflows/build-app-image.yml @@ -57,18 +57,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 # Login against a Docker registry to pull the GHCR image # https://github.com/docker/login-action - name: Log into registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -85,7 +85,7 @@ jobs: # Build image locally to enable to run the test # https://github.com/docker/build-push-action - name: Build Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: docker/Dockerfile diff --git a/builder/BaseScripts.php b/builder/BaseScripts.php index b8dee10..7d4be8a 100644 --- a/builder/BaseScripts.php +++ b/builder/BaseScripts.php @@ -14,7 +14,7 @@ class BaseScripts { - protected $workdir; + protected string|false $workdir; protected string $systemOs; public function __construct() @@ -47,9 +47,9 @@ public function fixDir($command) /** * Execute the given command by displaying console output live to the user. * - * @param string|array $cmd : command to be executed - * @return array exit_status : exit status of the executed command - * output : console output of the executed command + * @param array|string $cmd : command to be executed + * @return array|null exit_status : exit status of the executed command + * output : console output of the executed command * @throws ConfigException * @throws ConfigNotFoundException * @throws DependencyInjectionException @@ -58,7 +58,7 @@ public function fixDir($command) * @throws KeyNotFoundException * @throws ReflectionException */ - protected function liveExecuteCommand($cmd): ?array + protected function liveExecuteCommand(array|string $cmd): ?array { // while (@ ob_end_flush()); // end all output buffers if any @@ -122,7 +122,7 @@ protected function replaceVariables($variableValue) foreach ($args[0] as $arg) { $variableValue = str_replace( $arg, - Psr11::container()->get(substr($arg,1, -1)), + Psr11::get(substr($arg,1, -1)), $variableValue ); } diff --git a/builder/PostCreateScript.php b/builder/PostCreateScript.php index 727ec6e..14b781e 100755 --- a/builder/PostCreateScript.php +++ b/builder/PostCreateScript.php @@ -2,19 +2,21 @@ namespace Builder; -use ByJG\Util\JwtWrapper; +use ByJG\AnyDataset\Db\Factory; +use ByJG\JwtWrapper\JwtWrapper; use ByJG\Util\Uri; use Composer\Script\Event; +use Exception; use RecursiveCallbackFilterIterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; class PostCreateScript { - public function execute($workdir, $namespace, $composerName, $phpVersion, $mysqlConnection, $timezone) + public function execute($workdir, $namespace, $composerName, $phpVersion, $mysqlConnection, $timezone): void { // ------------------------------------------------ - // Defining function to interatively walking through the directories + // Defining function to interactively walking through the directories $directory = new RecursiveDirectoryIterator($workdir); $filter = new RecursiveCallbackFilterIterator($directory, function ($current/*, $key, $iterator*/) { // Skip hidden files and directories. @@ -50,8 +52,8 @@ public function execute($workdir, $namespace, $composerName, $phpVersion, $mysql foreach ($files as $file) { $contents = file_get_contents("$workdir/$file"); $contents = str_replace('ENV TZ=UTC', "ENV TZ=$timezone", $contents); - $contents = str_replace('php:8.1-fpm', "php:$phpVersion-fpm", $contents); - $contents = str_replace('php81', "php$phpVersionMSimple", $contents); + $contents = str_replace('php:8.3-fpm', "php:$phpVersion-fpm", $contents); + $contents = str_replace('php83', "php$phpVersionMSimple", $contents); file_put_contents( "$workdir/$file", $contents @@ -66,7 +68,6 @@ public function execute($workdir, $namespace, $composerName, $phpVersion, $mysql 'config/config-prod.php', 'config/config-test.php', 'docker-compose-dev.yml', - 'docker-compose-image.yml' ]; $uri = new Uri($mysqlConnection); foreach ($files as $file) { @@ -87,12 +88,12 @@ public function execute($workdir, $namespace, $composerName, $phpVersion, $mysql $objects = new RecursiveIteratorIterator($filter); foreach ($objects as $name => $object) { $contents = file_get_contents($name); - if (strpos($contents, 'RestReferenceArchitecture') !== false) { + if (str_contains($contents, 'RestReferenceArchitecture')) { echo "$name\n"; // Replace inside Quotes $contents = preg_replace( - "/([\'\"])RestReferenceArchitecture(.*?[\'\"])/", + "/(['\"])RestReferenceArchitecture(.*?['\"])/", '$1' . str_replace('\\', '\\\\\\\\', $namespace) . '$2', $contents ); @@ -114,8 +115,19 @@ public function execute($workdir, $namespace, $composerName, $phpVersion, $mysql ); } } + + shell_exec("composer update"); + shell_exec("git init"); + shell_exec("git branch -m main"); + shell_exec("git add ."); + shell_exec("git commit -m 'Initial commit'"); } + /** + * @param Event $event + * @return void + * @throws Exception + */ public static function run(Event $event) { $workdir = realpath(__DIR__ . '/..'); @@ -123,17 +135,57 @@ public static function run(Event $event) $currentPhpVersion = PHP_MAJOR_VERSION . "." .PHP_MINOR_VERSION; + $validatePHPVersion = function ($arg) { + $validPHPVersions = ['8.1', '8.2', '8.3']; + if (in_array($arg, $validPHPVersions)) { + return $arg; + } + throw new Exception('Only the PHP versions ' . implode(', ', $validPHPVersions) . ' are supported'); + }; + + $validateNamespace = function ($arg) { + if (empty($arg) || !preg_match('/^[A-Z][a-zA-Z0-9]*$/', $arg)) { + throw new Exception('Namespace must be one word in CamelCase'); + } + return $arg; + }; + + $validateComposer = function ($arg) { + if (empty($arg) || !preg_match('/^[a-z0-9-]+\/[a-z0-9-]+$/', $arg)) { + throw new Exception('Invalid Composer name'); + } + return $arg; + }; + + $validateURI = function ($arg) { + $uri = new Uri($arg); + if (empty($uri->getScheme())) { + throw new Exception('Invalid URI'); + } + Factory::getRegisteredDrivers($uri->getScheme()); + return $arg; + }; + + $validateTimeZone = function ($arg) { + if (empty($arg) || !in_array($arg, timezone_identifiers_list())) { + throw new Exception('Invalid Timezone'); + } + return $arg; + }; + + $maxRetries = 5; + $stdIo->write("========================================================"); $stdIo->write(" Setup Project"); $stdIo->write(" Answer the questions below"); $stdIo->write("========================================================"); $stdIo->write(""); $stdIo->write("Project Directory: " . $workdir); - $phpVersion = $stdIo->ask("PHP Version [$currentPhpVersion]: ", $currentPhpVersion); - $namespace = $stdIo->ask('Project namespace [MyRest]: ', 'MyRest'); - $composerName = $stdIo->ask('Composer name [me/myrest]: ', 'me/myrest'); - $mysqlConnection = $stdIo->ask('MySQL connection DEV [mysql://root:mysqlp455w0rd@mysql-container/mydb]: ', 'mysql://root:mysqlp455w0rd@mysql-container/mydb'); - $timezone = $stdIo->ask('Timezone [UTC]: ', 'UTC'); + $phpVersion = $stdIo->askAndValidate("PHP Version [$currentPhpVersion]: ", $validatePHPVersion, $maxRetries, $currentPhpVersion); + $namespace = $stdIo->askAndValidate('Project namespace [MyRest]: ', $validateNamespace, $maxRetries, 'MyRest'); + $composerName = $stdIo->askAndValidate('Composer name [me/myrest]: ', $validateComposer, $maxRetries, 'me/myrest'); + $mysqlConnection = $stdIo->askAndValidate('MySQL connection DEV [mysql://root:mysqlp455w0rd@mysql-container/mydb]: ', $validateURI, $maxRetries, 'mysql://root:mysqlp455w0rd@mysql-container/mydb'); + $timezone = $stdIo->askAndValidate('Timezone [UTC]: ', $validateTimeZone, $maxRetries, 'UTC'); $stdIo->ask('Press to continue'); $script = new PostCreateScript(); diff --git a/builder/Scripts.php b/builder/Scripts.php index 3f6b500..11710b7 100755 --- a/builder/Scripts.php +++ b/builder/Scripts.php @@ -11,6 +11,7 @@ use ByJG\DbMigration\Database\MySqlDatabase; use ByJG\DbMigration\Exception\InvalidMigrationFile; use ByJG\DbMigration\Migration; +use ByJG\JinjaPhp\Exception\TemplateParseException; use ByJG\JinjaPhp\Loader\FileSystemLoader; use ByJG\Util\Uri; use Composer\Script\Event; @@ -39,7 +40,7 @@ public function __construct() * @throws KeyNotFoundException * @throws ReflectionException */ - public static function migrate(Event $event) + public static function migrate(Event $event): void { $migrate = new Scripts(); $migrate->runMigrate($event->getArguments()); @@ -53,7 +54,7 @@ public static function migrate(Event $event) * @throws KeyNotFoundException * @throws ReflectionException */ - public static function genOpenApiDocs(Event $event) + public static function genOpenApiDocs(Event $event): void { $build = new Scripts(); $build->runGenOpenApiDocs($event->getArguments()); @@ -62,14 +63,16 @@ public static function genOpenApiDocs(Event $event) /** * @param Event $event * @return void + * @throws ConfigException * @throws ConfigNotFoundException * @throws DependencyInjectionException * @throws InvalidArgumentException + * @throws InvalidDateException * @throws KeyNotFoundException * @throws ReflectionException - * @throws \ByJG\Serializer\Exception\InvalidArgumentException + * @throws TemplateParseException */ - public static function codeGenerator(Event $event) + public static function codeGenerator(Event $event): void { $build = new Scripts(); $build->runCodeGenerator($event->getArguments()); @@ -86,8 +89,9 @@ public static function codeGenerator(Event $event) * @throws ReflectionException * @throws ConfigException * @throws InvalidDateException + * @throws Exception */ - public function runMigrate($arguments) + public function runMigrate($arguments): void { $argumentList = $this->extractArguments($arguments); if (isset($argumentList["command"])) { @@ -160,7 +164,7 @@ protected function extractArguments(array $arguments, bool $hasCmd = true): arra * @param array $arguments * @return void */ - public function runGenOpenApiDocs(array $arguments) + public function runGenOpenApiDocs(array $arguments): void { $docPath = $this->workdir . '/public/docs/'; @@ -179,15 +183,17 @@ public function runGenOpenApiDocs(array $arguments) /** * @param array $arguments * @return void + * @throws ConfigException * @throws ConfigNotFoundException * @throws DependencyInjectionException * @throws InvalidArgumentException + * @throws InvalidDateException * @throws KeyNotFoundException * @throws ReflectionException - * @throws \ByJG\Serializer\Exception\InvalidArgumentException + * @throws TemplateParseException * @throws Exception */ - public function runCodeGenerator(array $arguments) + public function runCodeGenerator(array $arguments): void { // Get Table Name $table = null; @@ -217,10 +223,11 @@ public function runCodeGenerator(array $arguments) $save = in_array("--save", $arguments); /** @var DbDriverInterface $dbDriver */ - $dbDriver = Psr11::container()->get(DbDriverInterface::class); + $dbDriver = Psr11::get(DbDriverInterface::class); $tableDefinition = $dbDriver->getIterator("EXPLAIN " . strtolower($table))->toArray(); $tableIndexes = $dbDriver->getIterator("SHOW INDEX FROM " . strtolower($table))->toArray(); + $autoIncrement = false; // Convert DB Types to PHP Types foreach ($tableDefinition as $key => $field) { @@ -230,6 +237,10 @@ public function runCodeGenerator(array $arguments) return strtoupper($matches[1]); }, $field['field']); + if ($field['extra'] == 'auto_increment') { + $autoIncrement = true; + } + switch ($type) { case 'int': case 'tinyint': @@ -292,7 +303,7 @@ public function runCodeGenerator(array $arguments) } } - // Create an array with non nullable fields but primary keys + // Create an array with non-nullable fields but primary keys $nonNullableFields = []; foreach ($tableDefinition as $field) { if ($field['null'] == 'NO' && $field['key'] != 'PRI') { @@ -300,7 +311,7 @@ public function runCodeGenerator(array $arguments) } } - // Create an array with non nullable fields but primary keys + // Create an array with non-nullable fields but primary keys foreach ($tableIndexes as $key => $field) { $tableIndexes[$key]['camelColumnName'] = preg_replace_callback('/_(.?)/', function($match) { return strtoupper($match[1]); @@ -309,6 +320,7 @@ public function runCodeGenerator(array $arguments) $data = [ 'namespace' => 'RestReferenceArchitecture', + 'autoIncrement' => $autoIncrement ? 'yes' : 'no', 'restTag' => ucwords(explode('_', strtolower($table))[0]), 'restPath' => str_replace('_', '/', strtolower($table)), 'className' => preg_replace_callback('/(?:^|_)(.?)/', function($match) { @@ -373,7 +385,7 @@ public function runCodeGenerator(array $arguments) echo "Processing Test for table $table...\n"; $template = $loader->getTemplate('test.php'); if ($save) { - $file = __DIR__ . '/../tests/Functional/Rest/' . $data['className'] . 'Test.php'; + $file = __DIR__ . '/../tests/Rest/' . $data['className'] . 'Test.php'; file_put_contents($file, $template->render($data)); echo "File saved in $file\n"; } else { diff --git a/composer.json b/composer.json index 7a4f1e0..5069cdc 100644 --- a/composer.json +++ b/composer.json @@ -5,22 +5,22 @@ "prefer-stable": true, "license": "MIT", "require": { - "php": ">=8.1", + "php": ">=8.1 <8.4", "ext-json": "*", "ext-openssl": "*", "ext-curl": "*", - "byjg/config": "^4.9", - "byjg/anydataset-db": "^4.9", - "byjg/micro-orm": "^4.9", - "byjg/authuser": "^4.9", - "byjg/mailwrapper": "^4.9", - "byjg/restserver": "^4.9", + "byjg/config": "^5.0", + "byjg/anydataset-db": "^5.0", + "byjg/micro-orm": "^5.0", + "byjg/authuser": "^5.0", + "byjg/mailwrapper": "^5.0", + "byjg/restserver": "^5.0", "zircote/swagger-php": "^4.6.1", - "byjg/swagger-test": "^4.9", - "byjg/migration": "^4.9", - "byjg/php-daemonize": "^4.9", - "byjg/shortid": "^4.9", - "byjg/jinja-php": "^4.9" + "byjg/swagger-test": "^5.0", + "byjg/migration": "^5.0", + "byjg/php-daemonize": "^5.0", + "byjg/shortid": "^5.0", + "byjg/jinja-php": "^5.0" }, "require-dev": { "phpunit/phpunit": "5.7.*|7.4.*|^9.5" @@ -36,7 +36,7 @@ "migrate": "Builder\\Scripts::migrate", "codegen": "Builder\\Scripts::codeGenerator", "openapi": "Builder\\Scripts::genOpenApiDocs", - "compile": "composer run openapi && composer run test", + "compile": "git pull && composer run openapi && composer run test", "up-local-dev": "docker compose -f docker-compose-dev.yml up -d", "down-local-dev": "docker compose -f docker-compose-dev.yml down", "post-create-project-cmd": "Builder\\PostCreateScript::run" diff --git a/config/config-dev.env b/config/config-dev.env index 39aed66..0f333ee 100644 --- a/config/config-dev.env +++ b/config/config-dev.env @@ -5,5 +5,5 @@ API_SERVER=localhost API_SCHEMA=http DBDRIVER_CONNECTION=mysql://root:mysqlp455w0rd@mysql-container/mydb EMAIL_CONNECTION=smtp://username:password@mail.example.com -EMAIL_TRANSACTIONAL_FROM=No Reply +EMAIL_TRANSACTIONAL_FROM='No Reply ' CORS_SERVERS=.* diff --git a/config/config-dev.php b/config/config-dev.php index ad76512..ccb7393 100644 --- a/config/config-dev.php +++ b/config/config-dev.php @@ -12,6 +12,9 @@ use ByJG\Config\DependencyInjection as DI; use ByJG\Config\Param; use ByJG\JinjaPhp\Loader\FileSystemLoader; +use ByJG\JwtWrapper\JwtHashHmacSecret; +use ByJG\JwtWrapper\JwtKeyInterface; +use ByJG\JwtWrapper\JwtWrapper; use ByJG\Mail\Envelope; use ByJG\Mail\MailerFactory; use ByJG\Mail\Wrapper\FakeSenderWrapper; @@ -22,8 +25,6 @@ use ByJG\RestServer\Middleware\JwtMiddleware; use ByJG\RestServer\OutputProcessor\JsonCleanOutputProcessor; use ByJG\RestServer\Route\OpenApiRouteList; -use ByJG\Util\JwtKeySecret; -use ByJG\Util\JwtWrapper; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use RestReferenceArchitecture\Model\User; @@ -49,16 +50,16 @@ ->withFactoryMethod('getInstance', [file_get_contents(__DIR__ . '/../public/docs/openapi.json')]) ->toSingleton(), - JwtKeySecret::class => DI::bind(JwtKeySecret::class) + JwtKeyInterface::class => DI::bind(JwtHashHmacSecret::class) ->withConstructorArgs(['jwt_super_secret_key']) ->toSingleton(), JwtWrapper::class => DI::bind(JwtWrapper::class) - ->withConstructorArgs([Param::get('API_SERVER'), Param::get(JwtKeySecret::class)]) + ->withConstructorArgs([Param::get('API_SERVER'), Param::get(JwtKeyInterface::class)]) ->toSingleton(), MailWrapperInterface::class => function () { - $apiKey = Psr11::container()->get('EMAIL_CONNECTION'); + $apiKey = Psr11::get('EMAIL_CONNECTION'); MailerFactory::registerMailer(MailgunApiWrapper::class); MailerFactory::registerMailer(FakeSenderWrapper::class); @@ -118,7 +119,7 @@ ->toSingleton(), 'CORS_SERVER_LIST' => function () { - return preg_split('/,(?![^{}]*})/', Psr11::container()->get('CORS_SERVERS')); + return preg_split('/,(?![^{}]*})/', Psr11::get('CORS_SERVERS')); }, JwtMiddleware::class => DI::bind(JwtMiddleware::class) @@ -159,7 +160,7 @@ if (Psr11::environment()->getCurrentEnvironment() != "prod") { $prefix = "[" . Psr11::environment()->getCurrentEnvironment() . "] "; } - return new Envelope(Psr11::container()->get('EMAIL_TRANSACTIONAL_FROM'), $to, $prefix . $subject, $body, true); + return new Envelope(Psr11::get('EMAIL_TRANSACTIONAL_FROM'), $to, $prefix . $subject, $body, true); }, ]; diff --git a/config/config-prod.php b/config/config-prod.php index eb7e468..564120f 100644 --- a/config/config-prod.php +++ b/config/config-prod.php @@ -1,10 +1,11 @@ DI::bind(JwtKeySecret::class) + JwtKeyInterface::class => DI::bind(JwtHashHmacSecret::class) ->withConstructorArgs(['jwt_super_secret_key']) ->toSingleton(), ]; diff --git a/config/config-staging.php b/config/config-staging.php index b4fda16..7637465 100644 --- a/config/config-staging.php +++ b/config/config-staging.php @@ -3,13 +3,14 @@ use ByJG\Cache\Psr16\BaseCacheEngine; use ByJG\Cache\Psr16\FileSystemCacheEngine; use ByJG\Config\DependencyInjection as DI; -use ByJG\Util\JwtKeySecret; +use ByJG\JwtWrapper\JwtHashHmacSecret; +use ByJG\JwtWrapper\JwtKeyInterface; return [ BaseCacheEngine::class => DI::bind(FileSystemCacheEngine::class)->toSingleton(), - JwtKeySecret::class => DI::bind(JwtKeySecret::class) + JwtKeyInterface::class => DI::bind(JwtHashHmacSecret::class) ->withConstructorArgs(['jwt_super_secret_key']) ->toSingleton(), diff --git a/config/config-test.php b/config/config-test.php index 3443895..e36bc90 100644 --- a/config/config-test.php +++ b/config/config-test.php @@ -1,10 +1,11 @@ DI::bind(JwtKeySecret::class) + JwtKeyInterface::class => DI::bind(JwtHashHmacSecret::class) ->withConstructorArgs(['jwt_super_secret_key']) ->toSingleton(), ]; diff --git a/db/migrations/down/00000.sql b/db/migrations/down/00000-rollback-table-users.sql similarity index 100% rename from db/migrations/down/00000.sql rename to db/migrations/down/00000-rollback-table-users.sql diff --git a/db/migrations/up/00001.sql b/db/migrations/up/00001-create-table-users.sql similarity index 100% rename from db/migrations/up/00001.sql rename to db/migrations/up/00001-create-table-users.sql diff --git a/docker-compose-image.yml b/docker-compose-image.yml deleted file mode 100644 index 7a3dd91..0000000 --- a/docker-compose-image.yml +++ /dev/null @@ -1,34 +0,0 @@ -version: '3.2' -services: - rest: - image: resttest:dev - container_name: resttest - build: - context: . - dockerfile: docker/Dockerfile - ports: - - "8080:80" - environment: - - APP_ENV=dev - networks: - - net - - mysql-container: - image: mysql:8.0 - container_name: mysql-container - environment: - MYSQL_ROOT_PASSWORD: mysqlp455w0rd - TZ: UTC - volumes: - - mysql-volume:/var/lib/mysql - ports: - - "3306:3306" - networks: - - net - -volumes: - mysql-volume: - -networks: - net: - diff --git a/docker/Dockerfile b/docker/Dockerfile index a08a37f..87e9347 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM byjg/php:8.1-fpm-nginx +FROM byjg/php:8.3-fpm-nginx-2025.03 # Refer to the documentation to setup the environment variables # https://github.com/byjg/docker-php/blob/master/docs/environment.md @@ -10,7 +10,7 @@ WORKDIR /srv # Setup Docker/Fpm -COPY ./docker/fpm/php /etc/php81/conf.d +COPY ./docker/fpm/php /etc/php83/conf.d COPY ./docker/nginx/conf.d /etc/nginx/http.d/ # Setup DateFile @@ -31,4 +31,5 @@ COPY templates /srv/templates COPY composer.* /srv COPY phpunit.xml.dist /srv COPY db /srv/db -RUN composer install --no-dev --no-interaction --no-progress --no-scripts --optimize-autoloader +RUN composer install --no-dev --no-interaction --no-progress --no-scripts --optimize-autoloader && \ + composer dump-autoload --optimize --classmap-authoritative -q diff --git a/docker/fpm/php/custom.ini b/docker/fpm/php/custom.ini index 2f645c1..a64bed2 100644 --- a/docker/fpm/php/custom.ini +++ b/docker/fpm/php/custom.ini @@ -1,3 +1,5 @@ +expose_php = off + date.timezone = UTC max_execution_time = 300 diff --git a/docs/getting_started.md b/docs/getting_started.md index 6992862..2ff6294 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -2,11 +2,14 @@ ## Requirements -Docker engine, PHP and an IDE. +- **Docker Engine**: For containerizing your application +- **PHP 8.3+**: For local development +- **IDE**: Any development environment of your choice -You'll need PHP 8.1 or higher installed in your machine. Preferrable the same version as you want to work with your project. +> **Windows Users**: If you don't have PHP installed in WSL2, please follow the [Windows](windows.md) guide. + +### Required PHP Extensions -Required PHP extensions: - ctype - curl - dom @@ -27,19 +30,21 @@ Required PHP extensions: ## Installation -```bash -mkdir ~/tutorial -composer create-project byjg/rest-reference-architecture ~/tutorial 4.9.* -``` +Choose one of the following installation methods: -or the latest development version: +```shell script +# Standard installation +mkdir ~/tutorial +composer create-project byjg/rest-reference-architecture ~/tutorial 5.0.* -```bash +# OR Latest development version mkdir ~/tutorial composer -sdev create-project byjg/rest-reference-architecture ~/tutorial master ``` -This process will ask some questions to setup your project. You can use the following below as a guide: +### Setup Configuration + +The installation will prompt you for configuration details: ```text > Builder\PostCreateScript::run @@ -48,43 +53,38 @@ This process will ask some questions to setup your project. You can use the foll Answer the questions below ======================================================== -Project Directory: /tmp/tutorial -PHP Version [7.4]: 8.1 +Project Directory: ~/tutorial +PHP Version [8.3]: 8.3 Project namespace [MyRest]: Tutorial Composer name [me/myrest]: MySQL connection DEV [mysql://root:mysqlp455w0rd@mysql-container/mydb]: -Timezone [UTC]: -Press to continue +Timezone [UTC]: ``` -Tip: The docker composer will create MySQL container named as `mysql-container` ([ref](https://github.com/byjg/php-rest-template/blob/master/docker-compose-dev.yml#L20)). -If you want to be able to access your MySQL container from your machine you need to add the following entry in your `/etc/hosts` file: - +**Tip**: To access the MySQL container locally, add this to your `/etc/hosts` file: ``` 127.0.0.1 mysql-container ``` - ## Running the Project -```bash +```shell cd ~/tutorial -docker-compose -f docker-compose-dev.yml up -d +docker compose -f docker-compose-dev.yml up -d ``` -## Creating the Database +## Database Setup -```bash -# Important this will destroy ALL DB data and create a fresh new database based on the migration +```shell +# Create a fresh database (warning: destroys existing data) APP_ENV=dev composer run migrate -- reset --yes -# *IF* your local PHP is not properly setup you can run this instead: -# export CONTAINER_NAME=# it is the second part of the composer name. e.g. me/myrest, it should be "myrest" +# Alternative if local PHP isn't configured: +# export CONTAINER_NAME=myrest # second part of your composer name # docker exec -it $CONTAINER_NAME composer run migrate -- reset --yes ``` -The result should be: - +Expected output: ```text > Builder\Scripts::migrate > Command: reset @@ -92,32 +92,31 @@ Doing reset, 0 Doing migrate, 1 ``` -## Testing the Project +## Verify Installation -```bash +```shell script curl http://localhost:8080/sample/ping ``` -The result: - +Expected response: ```json {"result":"pong"} ``` -## Running the Unit Tests - -```bash -APP_ENV=dev composer run test # Alternatively you can run `./vendor/bin/phpunit` +## Run Tests +```shell script +APP_ENV=dev composer run test # OR: docker exec -it $CONTAINER_NAME composer run test ``` -## Accessing the Swagger Documentation +## Documentation -```bash +Access the Swagger documentation: +```shell script open http://localhost:8080/docs ``` -## Continue the Tutorial +## Next Steps -You can continue this tutorial by following the next step: [creating a new table and crud](getting_started_01_create_table.md). +Continue with [creating a new table and CRUD operations](getting_started_01_create_table.md). diff --git a/docs/getting_started_01_create_table.md b/docs/getting_started_01_create_table.md index 3178a4b..8356555 100644 --- a/docs/getting_started_01_create_table.md +++ b/docs/getting_started_01_create_table.md @@ -1,13 +1,12 @@ # Getting Started - Creating a Table -After [create the project](getting_started.md) you can start to create your own tables. +After [creating the project](getting_started.md), you're ready to create your own tables. -## Create the table +## Create the Table -You need to create a new file in the `migrations` folder. The file name must be in the format `0000X.sql` where `X` is a number. -The number is used to order the execution of the scripts. +Create a new migration file in the `migrations` folder using the format `0000X-message.sql`, where `X` represents a sequential number that determines execution order. -Create a file `db/migrations/up/00002.sql` with the following content: +1. Create an "up" migration file `db/migrations/up/00002-create-table-example.sql`: ```sql create table example_crud @@ -19,30 +18,30 @@ create table example_crud ); ``` -To have consistency, we need to create the down script. The down script is used to rollback the changes. -Create a file `db/migrations/down/00001.sql` with the following content: +2. Create a corresponding "down" migration file `db/migrations/down/00001-rollback-table-example.sql` for rollbacks: ```sql drop table example_crud; ``` -## Run the migration +## Run the Migration -```bash +Apply your migrations with: + +```shell APP_ENV=dev composer run migrate -- update ``` -The result should be: - +Expected output: ```text > Builder\Scripts::migrate > Command: update Doing migrate, 2 ``` -If you want to rollback the changes: +To rollback changes: -```bash +```shell APP_ENV=dev composer run migrate -- update --up-to=1 ``` @@ -54,79 +53,55 @@ The result should be: Doing migrate, 1 ``` -## Generate the CRUD +Remember to run the migrate update again to apply the changes. -```bash -APP_ENV=dev composer run migrate -- update # Make sure DB is update -APP_ENV=dev composer run codegen -- --table example_crud --save all # (can be rest, model, test, repo, config) -``` -This will create the following files: +## Generate CRUD Components with the Code Generator -- ./src/Rest/ExampleCrudRest.php -- ./src/Model/ExampleCrud.php -- ./src/Repository/ExampleCrudRepository.php -- ./tests/Functional/Rest/ExampleCrudTest.php +Generate all necessary files for your new table: -To finalize the setup we need to generate the config. -Run the command bellow copy it contents and save it into the file `config/config-dev.php` +```shell +# Ensure DB is updated first +APP_ENV=dev composer run migrate -- update -```bash -APP_ENV=dev composer run codegen -- --table example_crud config +# Generate files (options: rest, model, test, repo, config, or all) +APP_ENV=dev composer run codegen -- --table example_crud --save all ``` -## First test - -The CodeGen is able to create the Unit Test for you. - -It is available in the file `tests/Functional/Rest/ExampleCrudTest.php`. +This creates: +- `./src/Rest/ExampleCrudRest.php` +- `./src/Model/ExampleCrud.php` +- `./src/Repository/ExampleCrudRepository.php` +- `./tests/Functional/Rest/ExampleCrudTest.php` -And you can run by invoking the command: +You have a manual step to generate the configuration by running the command below and adding it to `config/config-dev.php` -```bash -composer run test -``` - -This first test will fail because we don't have the endpoint yet. - -```text -ERRORS! -Tests: 36, Assertions: 104, Errors: 2, Failures: 6. -Script ./vendor/bin/phpunit handling the test event returned with error code 2 +```shell +APP_ENV=dev composer run codegen -- --table example_crud config ``` -Let's create them now. - -## Generate the endpoints from the OpenAPI Documentation +## Run the Tests -The OpenAPI documentation is generated automatically based on the code. -It is an important step because the documentation is used to create the endpoints and map them to the code. +The automatically generated test is located at `tests/Functional/Rest/ExampleCrudTest.php`. -If we don't generate the OpenAPI documentation, the new endpoints will not be available. +Run it: -```bash -composer run openapi -``` - -## Fixing the unit test - -Now, the endpoint errors passed, but the unit test still failing. - -```bash +```shell composer run test ``` -```text -PDOException: SQLSTATE[22007]: Invalid datetime format: 1292 Incorrect datetime value: 'birthdate' for column 'birthdate' at row 1 +Initial tests **_will fail_** because we need to: -ERRORS! -Tests: 36, Assertions: 111, Errors: 1. -Script ./vendor/bin/phpunit handling the test event returned with error code 2 +1. Generate OpenAPI documentation to create the endpoints: + +```shell +composer run openapi ``` -That's because the data used to test is not correct. +2. Fix the test data by updating `tests/Rest/ExampleCrudTest.php`: -Let's open the file `tests/Functional/Rest/ExampleCrudTest.php` and change the data to: + +Locate: ```php protected function getSampleData($array = false) @@ -140,26 +115,24 @@ Let's open the file `tests/Functional/Rest/ExampleCrudTest.php` and change the d ... ``` -Let's change the line: - -```text - 'birthdate' => 'birthdate', +And Change: +```php +'birthdate' => 'birthdate', ``` -to +To: -```text - 'birthdate' => '2023-01-01 00:00:00', +```php +'birthdate' => '2023-01-01 00:00:00', ``` -and run the unit test again: - -```bash +3. Run the tests again: +```shell composer run test ``` -And voila! The test passed! +Your tests should now pass successfully! -## Continue the Tutorial +## Next Steps -You can continue this tutorial by following the next step: [Add a new field](getting_started_02_add_new_field.md) +Continue with [Adding a New Field](getting_started_02_add_new_field.md) to enhance your implementation. diff --git a/docs/getting_started_02_add_new_field.md b/docs/getting_started_02_add_new_field.md index 669e4b6..783e94e 100644 --- a/docs/getting_started_02_add_new_field.md +++ b/docs/getting_started_02_add_new_field.md @@ -1,20 +1,20 @@ # Getting Started - Adding a new field to the Table -Now we have the table `example_crud` created in the [previous tutorial](getting_started_01_create_table.md), +Now we have the table `example_crud` created in the [previous tutorial](getting_started_01_create_table.md), let's modify it to add a new field `status`. ## Changing the table We need to add the proper field in the `up` script and remove it in the `down` script. -`db/migrations/up/00003.sql`: +`db/migrations/up/00003-add-field-status.sql`: ```sql alter table example_crud add status varchar(10) null; ``` -`db/migrations/down/00002.sql`: +`db/migrations/down/00002-rollback-field-status.sql`: ```sql alter table example_crud @@ -23,7 +23,7 @@ alter table example_crud ## Run the migration -```bash +```shell APP_ENV=dev composer run migrate -- update ``` @@ -40,6 +40,8 @@ Open the file: `src/Model/ExampleCrud.php` and add the field `status`: #[OA\Property(type: "string", format: "string", nullable: true)] protected ?string $status = null; + /** + * @return string|null */ public function getStatus(): ?string { @@ -60,17 +62,17 @@ Open the file: `src/Model/ExampleCrud.php` and add the field `status`: ## Adding the field status to the `Repository` -As we are just adding a new field, and we already updated the Model to support this new field +As we are just adding a new field, and we already updated the Model to support this new field we don't need to change the `Repository` class. ## Adding the field status to the `Rest` -We just need to allow the rest receive the new field. If we don't do it the API will throw an error. +We just need to allow the rest receive the new field. If we don't do it the API will throw an error. Open the file: `src/Rest/ExampleCrudRest.php` and add the attribute `status` to method `postExampleCrud()`: ```php - #[OA\RequestBody( +#[OA\RequestBody( description: "The object DummyHex to be created", required: true, content: new OA\JsonContent( @@ -90,13 +92,12 @@ Open the file: `src/Rest/ExampleCrudRest.php` and add the attribute `status` to ## Adding the field status to the `Test` We only need to change our method `getSample()` to return the status. -Open the file: `tests/Functional/Rest/ExampleCrudTest.php` +Open the file: `tests/Rest/ExampleCrudTest.php` ```php - protected function getSampleData($array = false) +protected function getSampleData($array = false) { $sample = [ - 'name' => 'name', 'birthdate' => '2023-01-01 00:00:00', 'code' => 1, @@ -107,7 +108,7 @@ Open the file: `tests/Functional/Rest/ExampleCrudTest.php` ## Update the OpenAPI -```bash +```shell composer run openapi ``` @@ -115,11 +116,10 @@ composer run openapi If everything is ok, the tests should pass: -```bash +```shell composer run test ``` ## Continue the tutorial -[Next: Creating a rest method](getting_started_03_create_rest_method.md) - +[Next: Creating a rest method](getting_started_03_create_rest_method.md) \ No newline at end of file diff --git a/docs/getting_started_03_create_rest_method.md b/docs/getting_started_03_create_rest_method.md index ab9024a..dabd282 100644 --- a/docs/getting_started_03_create_rest_method.md +++ b/docs/getting_started_03_create_rest_method.md @@ -1,31 +1,35 @@ -# Getting Started - Creating a Rest Method +I'll help you fix and improve this text. Let me analyze the Markdown document first. -This part of the tutorial we are going to create a new Rest Method to update the status of the `example_crud` table. +# Getting Started - Creating a REST Method -We will cover the following topics: +In this tutorial, we'll create a new REST method to update the status of the `example_crud` table. + +We'll cover the following topics: - OpenAPI Attributes -- Protect the endpoint -- Validate the input -- Save to the Database -- Return the result -- Unit Test +- Protecting the endpoint +- Validating input +- Saving to the database +- Returning results +- Unit testing ## OpenAPI Attributes -The first step is to add the OpenAPI attributes to the Rest Method. -We use the [zircote/swagger-php](https://zircote.github.io/swagger-php/guide/) library to add the attributes. +First, we'll add OpenAPI attributes to our REST method using +the [zircote/swagger-php](https://zircote.github.io/swagger-php/guide/) library. + +While the OpenAPI specification offers numerous attributes, we must define at least these three essential sets: -The list of OpenAPI attributes is to vast, however, there are a minimal of 3 sets of of attributes we must define. +### 1. Method Attribute -The first set is to define what will be the method attribute. It can be: +This defines the HTTP method: -- OA\Get - to retrieve data -- OA\Post - to create data -- OA\Put - For Update -- OA\Delete - For Delete/Cancel +- `OA\Get` - For retrieving data +- `OA\Post` - For creating data +- `OA\Put` - For updating data +- `OA\Delete` - For deleting/canceling data -e.g. +Example: ```php #[OA\Put( @@ -34,101 +38,96 @@ e.g. ["jwt-token" => []] ], tags: ["Example"], - description: 'Update the status of the ExampleCrud', + description: "Update the status of the ExampleCrud" )] ``` -The `security` attribute is used to define the security schema. If you don't define it, the endpoint will be public. +The `security` attribute defines the security schema. Without it, the endpoint remains public. + +### 2. Request Attribute -The second set it the request attribute. It can be, `OA\RequestBody` or `OA\Parameter` attribute. -It is used to define the input of the method. +This defines the input to the method using `OA\RequestBody` or `OA\Parameter`. -e.g. +Example: ```php #[OA\RequestBody( description: "The status to be updated", required: true, content: new OA\JsonContent( - required: [ "status" ], + required: ["status"], properties: [ new OA\Property(property: "id", type: "integer", format: "int32"), - new OA\Property(property: "status", type: "string", format: "string") + new OA\Property(property: "status", type: "string") ] ) )] ``` -The third set is the response attribute. It is `OA\Response` attribute. +### 3. Response Attribute + +This defines the expected output using `OA\Response`. ```php #[OA\Response( response: 200, - description: "The object rto be created", + description: "The operation result", content: new OA\JsonContent( - required: [ "result" ], + required: ["result"], properties: [ - new OA\Property(property: "result", type: "string", format: "string") + new OA\Property(property: "result", type: "string") ] ) )] ``` -The attributes need to be in the beginning of the method. The method can be anywhere in the code, -but the follow the pattern we will put it in the end of the class `ExampleCrudRest`. - -e.g +Place these attributes at the beginning of your method. Following our pattern, we'll add this method at the end of the `ExampleCrudRest` class: ```php - "123", "name" => "John Doe", - "role" => "admin" or "user" + "role" => "admin" // or "user" ] ``` -## Validate the input +## Validating Input -The next step is to validate the input. We will check if the request matches with the OpenAPI attributes. +Next, validate that the incoming request matches the OpenAPI specifications: ```php public function putExampleCrudStatus(HttpResponse $response, HttpRequest $request) @@ -141,66 +140,111 @@ public function putExampleCrudStatus(HttpResponse $response, HttpRequest $reques } ``` -## Call the Repository +## Updating Status in the Repository -As we have the payload with the correct information, we can call the repository to update the status. +After validating the payload, we can update the record status using the repository pattern: ```php +/** + * Update the status of an Example CRUD record + * + * @param HttpResponse $response + * @param HttpRequest $request + * @return void + */ public function putExampleCrudStatus(HttpResponse $response, HttpRequest $request) { -... - $exampleCrudRepo = Psr11::container()->get(ExampleCrudRepository::class); + // Previous code for payload validation... + + // Update the record status + $exampleCrudRepo = Psr11::get(ExampleCrudRepository::class); $model = $exampleCrudRepo->get($payload["id"]); + + if (!$model) { + throw new NotFoundException("Record not found"); + } + $model->setStatus($payload["status"]); $exampleCrudRepo->save($model); + + // Return response... +} ``` -## Return the result +## Returning the Response -The last step is to return the result as specified in the OpenAPI attributes. +After updating the record, we need to return a standardized response as specified in our OpenAPI schema: ```php public function putExampleCrudStatus(HttpResponse $response, HttpRequest $request) { -... + // Previous code for update logic... + + // Return standardized response $response->write([ "result" => "ok" ]); } ``` -## Unit Test +## Unit Testing -A vital piece of our code is to guarantee it will continue to run as expected. -To do that we need to create a unit test to validate the code. +To ensure our endpoint works correctly and continues to function as expected, we'll create a functional test. This test simulates calling the endpoint and validates both the response format and the business logic. -The test we will create is a functional test that will fake calling the endpoint -and validate the result if is matching with the OpenAPI attributes and if processing what is expected. - -We will add the test in the file `tests/Functional/Rest/ExampleCrudTest.php`. +Create or update the test file `tests/Functional/Rest/ExampleCrudTest.php`: ```php +/** + * @covers \YourNamespace\Controller\ExampleCrudController + */ public function testUpdateStatus() { - // If you need to login to get a token, use the code below - $result = json_decode($this->assertRequest(Credentials::requestLogin(Credentials::getAdminUser()))->getBody()->getContents(), true); + // Authenticate to get a valid token (if required) + $authResult = json_decode( + $this->assertRequest(Credentials::requestLogin(Credentials::getAdminUser())) + ->getBody() + ->getContents(), + true + ); + + // Prepare test data + $recordId = 1; + $newStatus = 'new status'; - // Execute the unit test - $request = new FakeApiRequester(); // It will mock the API call + // Create mock API request + $request = new FakeApiRequester(); $request - ->withPsr7Request($this->getPsr7Request()) // PSR7 Request to be used - ->withMethod('PUT') // Method to be used - ->withPath("/example/crud/status") // Path to be used - ->withRequestBody(json_encode([ // Request Body to be used - 'id' => 1, - 'status' => 'new status' + ->withPsr7Request($this->getPsr7Request()) + ->withMethod('PUT') + ->withPath("/example/crud/status") + ->withRequestBody(json_encode([ + 'id' => $recordId, + 'status' => $newStatus ])) - ->assertResponseCode(200) // Expected Response Code - ->withRequestHeader([ // If your method requires a token use this. - "Authorization" => "Bearer " . $result['token'] + ->withRequestHeader([ + "Authorization" => "Bearer " . $authResult['token'], + "Content-Type" => "application/json" ]) - ; - $body = $this->assertRequest($request); - $bodyAr = json_decode($body->getBody()->getContents(), true); // If necessary work with the result of the request + ->assertResponseCode(200); + + // Execute the request and get response + $response = $this->assertRequest($request); + $responseData = json_decode($response->getBody()->getContents(), true); + + // There is no necessary to Assert expected response format and data + // because the assertRequest will do it for you. + // $this->assertIsArray($responseData); + // $this->assertArrayHasKey('result', $responseData); + // $this->assertEquals('ok', $responseData['result']); + + // Verify the database was updated correctly + $repository = Psr11::get(ExampleCrudRepository::class); + $updatedRecord = $repository->get($recordId); + $this->assertEquals($newStatus, $updatedRecord->getStatus()); } ``` + +This test performs the following validations: +1. Ensures the endpoint returns a 200 status code +2. Verifies the response has the expected JSON structure +3. Confirms the database record was actually updated with the new status \ No newline at end of file diff --git a/docs/login.md b/docs/login.md index 3b4c5db..b81dbd7 100644 --- a/docs/login.md +++ b/docs/login.md @@ -82,7 +82,7 @@ Also, there is an endpoint to refresh the token. The endpoint is `/refresh` and To configure the key you can change here: ```php - JwtKeySecret::class => DI::bind(JwtKeySecret::class) + JwtKeyInterface::class => DI::bind(\ByJG\JwtWrapper\JwtHashHmacSecret::class) ->withConstructorArgs(['supersecretkeyyoushouldnotcommittogithub']) ->toSingleton(), ``` diff --git a/docs/psr11.md b/docs/psr11.md index 874173a..ca75c48 100644 --- a/docs/psr11.md +++ b/docs/psr11.md @@ -40,7 +40,7 @@ The configuration is loaded by the [byjg/config](https://github.com/byjg/config) You just need to: ```php -Psr11::container()->get('WEB_SERVER'); +Psr11::get('WEB_SERVER'); ``` ## Defining the available environments diff --git a/docs/psr11_di.md b/docs/psr11_di.md index 410ebe3..73b2bed 100644 --- a/docs/psr11_di.md +++ b/docs/psr11_di.md @@ -25,7 +25,7 @@ return [ To use in your code, you just need to set the environment variable `APP_ENV` to the environment name (`dev` or `prod`) and call: ```php -Psr11::container()->get(BaseCacheEngine::class); +Psr11::get(BaseCacheEngine::class); ``` The application will return the correct implementation based on the environment. diff --git a/docs/windows.md b/docs/windows.md new file mode 100644 index 0000000..da2f63b --- /dev/null +++ b/docs/windows.md @@ -0,0 +1,27 @@ +# Running on Windows Without PHP + +This project is primarily designed for Linux environments, but can be easily run on Windows using Docker. + +## Prerequisites + +- Docker Desktop installed and running on your Windows machine +- No need for a local PHP installation or WSL2 configuration + +## Quick Start + +1. Open Command Prompt or PowerShell +2. Navigate to your desired project location: + +```textmate +cd C:\Users\MyUser\Projects +``` + +3. Launch a containerized PHP environment with the following command: + +```textmate +docker run -it --rm -v %cd%:/root -w /root byjg/php:8.3-cli bash +``` + +4. Once inside the container shell, you can run all PHP commands normally as if you had PHP installed locally. + +When referencing the current directory, use `/root/something` instead of `~/something`. diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 39d5297..ff5e401 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,33 +6,36 @@ and open the template in the editor. --> - + stopOnFailure="false" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> - - - ./src - - + + + ./src + + - - - ./tests/ - - + + + ./tests/Rest + ./tests/ + + - - - - + + + + - - - + + + diff --git a/public/app.php b/public/app.php index 21d057f..a675491 100644 --- a/public/app.php +++ b/public/app.php @@ -10,8 +10,8 @@ class App { public static function run() { - $server = Psr11::container()->get(HttpRequestHandler::class); - $server->handle(Psr11::container()->get(OpenApiRouteList::class)); + $server = Psr11::get(HttpRequestHandler::class); + $server->handle(Psr11::get(OpenApiRouteList::class)); } } diff --git a/src/Model/Dummy.php b/src/Model/Dummy.php index 16a6c8f..3049d31 100644 --- a/src/Model/Dummy.php +++ b/src/Model/Dummy.php @@ -1,6 +1,9 @@ id; } /** * @param int|null $id - * @return Dummy + * @return $this */ - public function setId(?int $id): Dummy + public function setId(int|null $id): static { $this->id = $id; return $this; @@ -46,16 +52,16 @@ public function setId(?int $id): Dummy /** * @return string|null */ - public function getField(): ?string + public function getField(): string|null { return $this->field; } /** * @param string|null $field - * @return Dummy + * @return $this */ - public function setField(?string $field): Dummy + public function setField(string|null $field): static { $this->field = $field; return $this; diff --git a/src/Model/DummyHex.php b/src/Model/DummyHex.php index 95a1e21..ae317c8 100644 --- a/src/Model/DummyHex.php +++ b/src/Model/DummyHex.php @@ -1,6 +1,11 @@ id; } /** - * @param string|null $id - * @return DummyHex + * @param string|HexUuidLiteral|null $id + * @return $this */ - public function setId(?string $id): DummyHex + public function setId(string|HexUuidLiteral|null $id): static { $this->id = $id; return $this; @@ -52,16 +61,16 @@ public function setId(?string $id): DummyHex /** * @return string|null */ - public function getUuid(): ?string + public function getUuid(): string|null { return $this->uuid; } /** * @param string|null $uuid - * @return DummyHex + * @return $this */ - public function setUuid(?string $uuid): DummyHex + public function setUuid(string|null $uuid): static { $this->uuid = $uuid; return $this; @@ -70,16 +79,16 @@ public function setUuid(?string $uuid): DummyHex /** * @return string|null */ - public function getField(): ?string + public function getField(): string|null { return $this->field; } /** * @param string|null $field - * @return DummyHex + * @return $this */ - public function setField(?string $field): DummyHex + public function setField(string|null $field): static { $this->field = $field; return $this; diff --git a/src/Model/User.php b/src/Model/User.php index f3c6930..36f1186 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -4,10 +4,15 @@ use ByJG\Authenticate\Definition\PasswordDefinition; use ByJG\Authenticate\Model\UserModel; +use ByJG\Authenticate\Model\UserPropertiesModel; +use ByJG\MicroOrm\Attributes\TableAttribute; +use ByJG\MicroOrm\Literal\HexUuidLiteral; use Exception; +use InvalidArgumentException; use OpenApi\Attributes as OA; use RestReferenceArchitecture\Psr11; +#[TableAttribute("users")] #[OA\Schema(required: ["email"], type: "object", xml: new OA\Xml(name: "User"))] class User extends UserModel { @@ -26,60 +31,64 @@ class User extends UserModel const ROLE_USER = 'user'; /** - * @var ?string + * @var ?string|int|HexUuidLiteral */ #[OA\Property(type: "string", format: "string")] - protected $userid; + protected string|int|HexUuidLiteral|null $userid = null; /** * @var ?string */ #[OA\Property(type: "string", format: "string")] - protected $name; + protected ?string $name = null; /** * @var ?string */ #[OA\Property(type: "string", format: "string")] - protected $email; + protected ?string $email = null; /** * @var ?string */ #[OA\Property(type: "string", format: "string")] - protected $username; + protected ?string $username = null; + /** * @var ?string */ #[OA\Property(type: "string", format: "string")] - protected $password; + protected ?string $password = null; /** * @var ?string */ #[OA\Property(type: "string", format: "string")] - protected $created; + protected ?string $created = null; /** * @var ?string */ #[OA\Property(type: "string", format: "string")] - protected $updated; + protected ?string $updated = null; /** * @var ?string */ #[OA\Property(type: "string", format: "string")] - protected $admin = "no"; + protected ?string $admin = null; /** * @OA\Property() * @var ?string */ - protected $uuid; + protected ?string $uuid = null; + + protected array $propertyList = []; /** - * User constructor. + * UserModel constructor. + * * @param string $name * @param string $email * @param string $username @@ -87,11 +96,141 @@ class User extends UserModel * @param string $admin * @throws Exception */ - public function __construct(string $name = "", string $email = "", string $username = "", string $password = "", string $admin = "") + public function __construct(string $name = "", string $email = "", string $username = "", string $password = "", string $admin = "no") { parent::__construct($name, $email, $username, $password, $admin); - $this->withPasswordDefinition(Psr11::container()->get(PasswordDefinition::class)); + $this->withPasswordDefinition(Psr11::get(PasswordDefinition::class)); + } + + + /** + * @return string|HexUuidLiteral|int|null + */ + public function getUserid(): string|HexUuidLiteral|int|null + { + return $this->userid; + } + + /** + * @param string|HexUuidLiteral|int|null $userid + */ + public function setUserid(string|HexUuidLiteral|int|null $userid): void + { + $this->userid = $userid; + } + + /** + * @return string|null + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * @param string|null $name + */ + public function setName(?string $name): void + { + $this->name = $name; + } + + /** + * @return string|null + */ + public function getEmail(): ?string + { + return $this->email; + } + + /** + * @param string|null $email + */ + public function setEmail(?string $email): void + { + $this->email = $email; + } + + /** + * @return string|null + */ + public function getUsername(): ?string + { + return $this->username; + } + + /** + * @param string|null $username + */ + public function setUsername(?string $username): void + { + $this->username = $username; + } + + /** + * @return string|null + */ + public function getPassword(): ?string + { + return $this->password; + } + + /** + * @param string|null $password + */ + public function setPassword(?string $password): void + { + if (!empty($this->passwordDefinition) && !empty($password) && strlen($password) != 40) { + $result = $this->passwordDefinition->matchPassword($password); + if ($result != PasswordDefinition::SUCCESS) { + throw new InvalidArgumentException("Password does not match the password definition [{$result}]"); + } + } + $this->password = $password; + } + + /** + * @return string|null + */ + public function getCreated(): ?string + { + return $this->created; + } + + /** + * @param string|null $created + */ + public function setCreated(?string $created): void + { + $this->created = $created; + } + + /** + * @return string|null + */ + public function getAdmin(): ?string + { + return $this->admin; + } + + /** + * @param string|null $admin + */ + public function setAdmin(?string $admin): void + { + $this->admin = $admin; + } + + public function set(string $name, string|null $value): void + { + $property = $this->get($name, true); + if (empty($property)) { + $property = new UserPropertiesModel($name, $value ?? ""); + $this->addProperty($property); + } else { + $property->setValue($value); + } } /** diff --git a/src/OpenApiSpec.php b/src/OpenApiSpec.php index 808204a..c851915 100644 --- a/src/OpenApiSpec.php +++ b/src/OpenApiSpec.php @@ -1,6 +1,7 @@ build($env); } return self::$container; } + /** + * @param string $id + * @param mixed ...$parameters + * @return mixed + * @throws ConfigException + * @throws ConfigNotFoundException + * @throws DependencyInjectionException + * @throws InvalidArgumentException + * @throws KeyNotFoundException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws ReflectionException + */ + public static function get(string $id, mixed ...$parameters): mixed + { + return Psr11::container()->get($id, ...$parameters); + } + /** * @return Definition|null * @throws ConfigException @@ -50,7 +76,12 @@ public static function environment(): ?Definition ->addEnvironment($test) ->addEnvironment($staging) ->addEnvironment($prod) - ; + ->withOSEnvironment( + [ + 'TAG_VERSION', + 'TAG_COMMIT', + ] + ); } return self::$definition; diff --git a/src/Repository/BaseRepository.php b/src/Repository/BaseRepository.php index 24c4ea4..be2eb1a 100644 --- a/src/Repository/BaseRepository.php +++ b/src/Repository/BaseRepository.php @@ -11,7 +11,8 @@ use ByJG\MicroOrm\Exception\OrmBeforeInvalidException; use ByJG\MicroOrm\Exception\OrmInvalidFieldsException; use ByJG\MicroOrm\FieldMapping; -use ByJG\MicroOrm\Literal; +use ByJG\MicroOrm\Literal\HexUuidLiteral; +use ByJG\MicroOrm\Literal\Literal; use ByJG\MicroOrm\Mapper; use ByJG\MicroOrm\Query; use ByJG\MicroOrm\Repository; @@ -19,7 +20,6 @@ use ByJG\Serializer\Exception\InvalidArgumentException; use ReflectionException; use RestReferenceArchitecture\Psr11; -use RestReferenceArchitecture\Util\HexUuidLiteral; abstract class BaseRepository { @@ -36,7 +36,12 @@ abstract class BaseRepository */ public function get($itemId) { - return $this->repository->get($this->prepareUuidQuery($itemId)); + return $this->repository->get(HexUuidLiteral::create($itemId)); + } + + public function getRepository(): Repository + { + return $this->repository; } public function getMapper() @@ -55,28 +60,6 @@ public function getByQuery($query) return $this->repository->getByQuery($query); } - protected function prepareUuidQuery($itemId) - { - $result = []; - foreach ((array)$itemId as $item) { - if ($item instanceof Literal) { - $result[] = $item; - continue; - } - $hydratedItem = preg_replace('/[^0-9A-F\-]/', '', $item); - if (preg_match("/^\w{8}-?\w{4}-?\w{4}-?\w{4}-?\w{12}$/", $hydratedItem)) { - $result[] = new HexUuidLiteral($hydratedItem); - } else { - $result[] = $item; - } - } - - if (count($result) == 1) { - return $result[0]; - } - return $result; - } - /** * @param int|null $page * @param int $size @@ -104,7 +87,7 @@ public function listGeneric($tableName, $fields = [], $page = 0, $size = 20, $or $object = $query->build($this->repository->getDbDriver()); - $iterator = $this->repository->getDbDriver()->getIterator($object["sql"], $object["params"]); + $iterator = $this->repository->getDbDriver()->getIterator($object->getSql(), $object->getParameters()); return $iterator->toArray(); } @@ -150,7 +133,7 @@ public function model() public static function getClosureNewUUID(): \Closure { return function () { - return new Literal("X'" . Psr11::container()->get(DbDriverInterface::class)->getScalar("SELECT hex(uuid_to_bin(uuid()))") . "'"); + return new Literal("X'" . Psr11::get(DbDriverInterface::class)->getScalar("SELECT hex(uuid_to_bin(uuid()))") . "'"); }; } @@ -166,7 +149,7 @@ public static function getClosureNewUUID(): \Closure */ public static function getUuid() { - return Psr11::container()->get(DbDriverInterface::class)->getScalar("SELECT insert(insert(insert(insert(hex(uuid_to_bin(uuid())),9,0,'-'),14,0,'-'),19,0,'-'),24,0,'-')"); + return Psr11::get(DbDriverInterface::class)->getScalar("SELECT insert(insert(insert(insert(hex(uuid_to_bin(uuid())),9,0,'-'),14,0,'-'),19,0,'-'),24,0,'-')"); } /** @@ -178,26 +161,31 @@ public static function getUuid() protected function setClosureFixBinaryUUID(?Mapper $mapper, $binPropertyName = 'id', $uuidStrPropertyName = 'uuid') { $fieldMapping = FieldMapping::create($binPropertyName) - ->withUpdateFunction(function ($value, $instance) { - if (empty($value)) { - return null; - } - if (!($value instanceof Literal)) { - $value = new HexUuidLiteral($value); - } - return $value; - }) - ->withSelectFunction(function ($value, $instance) use ($binPropertyName, $uuidStrPropertyName) { - if (!empty($uuidStrPropertyName)) { - $fieldValue = $instance->{'get' . $uuidStrPropertyName}(); - } else { - $fieldValue = HexUuidLiteral::getFormattedUuid($instance->{'get' . $binPropertyName}(), false); + ->withUpdateFunction( + function ($value, $instance) { + if (empty($value)) { + return null; + } + if (!($value instanceof Literal)) { + $value = new HexUuidLiteral($value); + } + return $value; } - if (is_null($fieldValue)) { - return null; + ) + ->withSelectFunction( + function ($value, $instance) use ($binPropertyName, $uuidStrPropertyName) { + if (!empty($uuidStrPropertyName)) { + $fieldValue = $instance->{'get' . $uuidStrPropertyName}(); + } else { + $itemValue = $instance->{'get' . $binPropertyName}(); + $fieldValue = HexUuidLiteral::getFormattedUuid($itemValue, false, $itemValue); + } + if (is_null($fieldValue)) { + return null; + } + return $fieldValue; } - return $fieldValue; - }); + ); if (!empty($mapper)) { $mapper->addFieldMapping($fieldMapping); @@ -207,7 +195,7 @@ protected function setClosureFixBinaryUUID(?Mapper $mapper, $binPropertyName = ' } /** - * @param $model + * @param $model * @return mixed * @throws InvalidArgumentException * @throws OrmBeforeInvalidException @@ -221,7 +209,7 @@ public function save($model, ?UpdateConstraint $updateConstraint = null) $primaryKey = $this->repository->getMapper()->getPrimaryKey()[0]; if ($model->{"get$primaryKey"}() instanceof Literal) { - $model->{"set$primaryKey"}(HexUuidLiteral::getUuidFromLiteral($model->{"get$primaryKey"}())); + $model->{"set$primaryKey"}(HexUuidLiteral::create($model->{"get$primaryKey"}())); } return $model; diff --git a/src/Repository/DummyHexRepository.php b/src/Repository/DummyHexRepository.php index 56ba82f..b3e2c8e 100644 --- a/src/Repository/DummyHexRepository.php +++ b/src/Repository/DummyHexRepository.php @@ -3,9 +3,9 @@ namespace RestReferenceArchitecture\Repository; use ByJG\AnyDataset\Db\DbDriverInterface; -use ByJG\MicroOrm\FieldMapping; -use ByJG\MicroOrm\Mapper; +use ByJG\MicroOrm\Exception\OrmModelInvalidException; use ByJG\MicroOrm\Repository; +use ReflectionException; use RestReferenceArchitecture\Model\DummyHex; class DummyHexRepository extends BaseRepository @@ -14,22 +14,12 @@ class DummyHexRepository extends BaseRepository * DummyHexRepository constructor. * * @param DbDriverInterface $dbDriver - * + * @throws OrmModelInvalidException + * @throws ReflectionException */ public function __construct(DbDriverInterface $dbDriver) { - $mapper = new Mapper( - DummyHex::class, - 'dummyhex', - 'id' - ); - $mapper->withPrimaryKeySeedFunction(BaseRepository::getClosureNewUUID()); - - - $this->setClosureFixBinaryUUID($mapper); - $mapper->addFieldMapping(FieldMapping::create('uuid')->withFieldName('uuid')->withUpdateFunction(Mapper::doNotUpdateClosure())); - - $this->repository = new Repository($dbDriver, $mapper); + $this->repository = new Repository($dbDriver, DummyHex::class); } diff --git a/src/Repository/DummyRepository.php b/src/Repository/DummyRepository.php index 8400156..35948cb 100644 --- a/src/Repository/DummyRepository.php +++ b/src/Repository/DummyRepository.php @@ -3,9 +3,10 @@ namespace RestReferenceArchitecture\Repository; use ByJG\AnyDataset\Db\DbDriverInterface; -use ByJG\MicroOrm\Mapper; +use ByJG\MicroOrm\Exception\OrmModelInvalidException; use ByJG\MicroOrm\Query; use ByJG\MicroOrm\Repository; +use ReflectionException; use RestReferenceArchitecture\Model\Dummy; class DummyRepository extends BaseRepository @@ -14,23 +15,12 @@ class DummyRepository extends BaseRepository * DummyRepository constructor. * * @param DbDriverInterface $dbDriver - * + * @throws OrmModelInvalidException + * @throws ReflectionException */ public function __construct(DbDriverInterface $dbDriver) { - $mapper = new Mapper( - Dummy::class, - 'dummy', - 'id' - ); - // $mapper->withPrimaryKeySeedFunction(BaseRepository::getClosureNewUUID()); - - - // Table UUID Definition - // $this->setClosureFixBinaryUUID($mapper); - - - $this->repository = new Repository($dbDriver, $mapper); + $this->repository = new Repository($dbDriver, Dummy::class); } diff --git a/src/Repository/UserDefinition.php b/src/Repository/UserDefinition.php index 1e2af64..51d7fee 100644 --- a/src/Repository/UserDefinition.php +++ b/src/Repository/UserDefinition.php @@ -4,9 +4,9 @@ use ByJG\AnyDataset\Db\DbDriverInterface; use ByJG\Authenticate\Model\UserModel; -use ByJG\MicroOrm\Literal; +use ByJG\MicroOrm\Literal\HexUuidLiteral; +use ByJG\MicroOrm\Literal\Literal; use RestReferenceArchitecture\Psr11; -use RestReferenceArchitecture\Util\HexUuidLiteral; class UserDefinition extends \ByJG\Authenticate\Definition\UserDefinition { @@ -18,7 +18,7 @@ public function __construct($table = 'users', $model = UserModel::class, $loginF $this->markPropertyAsReadOnly("created"); $this->markPropertyAsReadOnly("updated"); $this->defineGenerateKeyClosure(function () { - return new Literal("X'" . Psr11::container()->get(DbDriverInterface::class)->getScalar("SELECT hex(uuid_to_bin(uuid()))") . "'"); + return new Literal("X'" . Psr11::get(DbDriverInterface::class)->getScalar("SELECT hex(uuid_to_bin(uuid()))") . "'"); } ); diff --git a/src/Rest/DummyHexRest.php b/src/Rest/DummyHexRest.php index ca65cf6..ee2bd68 100644 --- a/src/Rest/DummyHexRest.php +++ b/src/Rest/DummyHexRest.php @@ -16,7 +16,7 @@ use ByJG\RestServer\Exception\Error404Exception; use ByJG\RestServer\HttpRequest; use ByJG\RestServer\HttpResponse; -use ByJG\Serializer\BinderObject; +use ByJG\Serializer\ObjectCopy; use OpenApi\Attributes as OA; use ReflectionException; use RestReferenceArchitecture\Model\DummyHex; @@ -70,7 +70,7 @@ public function getDummyHex(HttpResponse $response, HttpRequest $request): void { JwtContext::requireAuthenticated($request); - $dummyHexRepo = Psr11::container()->get(DummyHexRepository::class); + $dummyHexRepo = Psr11::get(DummyHexRepository::class); $id = $request->param('id'); $result = $dummyHexRepo->get($id); @@ -156,7 +156,7 @@ public function listDummyHex(HttpResponse $response, HttpRequest $request): void { JwtContext::requireAuthenticated($request); - $repo = Psr11::container()->get(DummyHexRepository::class); + $repo = Psr11::get(DummyHexRepository::class); $page = $request->get('page'); $size = $request->get('size'); @@ -232,9 +232,9 @@ public function postDummyHex(HttpResponse $response, HttpRequest $request): void $payload = OpenApiContext::validateRequest($request); $model = new DummyHex(); - BinderObject::bind($payload, $model); + ObjectCopy::copy($payload, $model); - $dummyHexRepo = Psr11::container()->get(DummyHexRepository::class); + $dummyHexRepo = Psr11::get(DummyHexRepository::class); $dummyHexRepo->save($model); $response->write([ "id" => $model->getId()]); @@ -290,12 +290,12 @@ public function putDummyHex(HttpResponse $response, HttpRequest $request): void $payload = OpenApiContext::validateRequest($request); - $dummyHexRepo = Psr11::container()->get(DummyHexRepository::class); + $dummyHexRepo = Psr11::get(DummyHexRepository::class); $model = $dummyHexRepo->get($payload['id']); if (empty($model)) { throw new Error404Exception('Id not found'); } - BinderObject::bind($payload, $model); + ObjectCopy::copy($payload, $model); $dummyHexRepo->save($model); } diff --git a/src/Rest/DummyRest.php b/src/Rest/DummyRest.php index 80a7373..0654064 100644 --- a/src/Rest/DummyRest.php +++ b/src/Rest/DummyRest.php @@ -16,7 +16,7 @@ use ByJG\RestServer\Exception\Error404Exception; use ByJG\RestServer\HttpRequest; use ByJG\RestServer\HttpResponse; -use ByJG\Serializer\BinderObject; +use ByJG\Serializer\ObjectCopy; use OpenApi\Attributes as OA; use ReflectionException; use RestReferenceArchitecture\Model\Dummy; @@ -70,7 +70,7 @@ public function getDummy(HttpResponse $response, HttpRequest $request): void { JwtContext::requireAuthenticated($request); - $dummyRepo = Psr11::container()->get(DummyRepository::class); + $dummyRepo = Psr11::get(DummyRepository::class); $id = $request->param('id'); $result = $dummyRepo->get($id); @@ -156,7 +156,7 @@ public function listDummy(HttpResponse $response, HttpRequest $request): void { JwtContext::requireAuthenticated($request); - $repo = Psr11::container()->get(DummyRepository::class); + $repo = Psr11::get(DummyRepository::class); $page = $request->get('page'); $size = $request->get('size'); @@ -232,9 +232,9 @@ public function postDummy(HttpResponse $response, HttpRequest $request): void $payload = OpenApiContext::validateRequest($request); $model = new Dummy(); - BinderObject::bind($payload, $model); + ObjectCopy::copy($payload, $model); - $dummyRepo = Psr11::container()->get(DummyRepository::class); + $dummyRepo = Psr11::get(DummyRepository::class); $dummyRepo->save($model); $response->write([ "id" => $model->getId()]); @@ -290,12 +290,12 @@ public function putDummy(HttpResponse $response, HttpRequest $request): void $payload = OpenApiContext::validateRequest($request); - $dummyRepo = Psr11::container()->get(DummyRepository::class); + $dummyRepo = Psr11::get(DummyRepository::class); $model = $dummyRepo->get($payload['id']); if (empty($model)) { throw new Error404Exception('Id not found'); } - BinderObject::bind($payload, $model); + ObjectCopy::copy($payload, $model); $dummyRepo->save($model); } diff --git a/src/Rest/Login.php b/src/Rest/Login.php index 4ede128..e98d04b 100644 --- a/src/Rest/Login.php +++ b/src/Rest/Login.php @@ -4,16 +4,16 @@ use ByJG\Authenticate\UsersDBDataset; use ByJG\Mail\Wrapper\MailWrapperInterface; +use ByJG\MicroOrm\Literal\HexUuidLiteral; use ByJG\RestServer\Exception\Error401Exception; use ByJG\RestServer\Exception\Error422Exception; use ByJG\RestServer\HttpRequest; use ByJG\RestServer\HttpResponse; -use ByJG\RestServer\ResponseBag; +use ByJG\RestServer\SerializationRuleEnum; use OpenApi\Attributes as OA; use RestReferenceArchitecture\Model\User; use RestReferenceArchitecture\Psr11; use RestReferenceArchitecture\Repository\BaseRepository; -use RestReferenceArchitecture\Util\HexUuidLiteral; use RestReferenceArchitecture\Util\JwtContext; use RestReferenceArchitecture\Util\OpenApiContext; @@ -55,15 +55,13 @@ class Login )] public function post(HttpResponse $response, HttpRequest $request) { - OpenApiContext::validateRequest($request); + $json = OpenApiContext::validateRequest($request); - $json = json_decode($request->payload()); - - $users = Psr11::container()->get(UsersDBDataset::class); - $user = $users->isValidUser($json->username, $json->password); + $users = Psr11::get(UsersDBDataset::class); + $user = $users->isValidUser($json["username"], $json["password"]); $metadata = JwtContext::createUserMetadata($user); - $response->getResponseBag()->setSerializationRule(ResponseBag::SINGLE_OBJECT); + $response->getResponseBag()->setSerializationRule(SerializationRuleEnum::SingleObject); $response->write(['token' => JwtContext::createToken($metadata)]); $response->write(['data' => $metadata]); } @@ -109,12 +107,12 @@ public function refreshToken(HttpResponse $response, HttpRequest $request) throw new Error401Exception("You only can refresh the token 5 minutes before expire"); } - $users = Psr11::container()->get(UsersDBDataset::class); + $users = Psr11::get(UsersDBDataset::class); $user = $users->getById(new HexUuidLiteral(JwtContext::getUserId())); $metadata = JwtContext::createUserMetadata($user); - $response->getResponseBag()->setSerializationRule(ResponseBag::SINGLE_OBJECT); + $response->getResponseBag()->setSerializationRule(SerializationRuleEnum::SingleObject); $response->write(['token' => JwtContext::createToken($metadata)]); $response->write(['data' => $metadata]); @@ -149,12 +147,10 @@ public function refreshToken(HttpResponse $response, HttpRequest $request) )] public function postResetRequest(HttpResponse $response, HttpRequest $request) { - OpenApiContext::validateRequest($request); - - $json = json_decode($request->payload()); + $json = OpenApiContext::validateRequest($request); - $users = Psr11::container()->get(UsersDBDataset::class); - $user = $users->getByEmail($json->email); + $users = Psr11::get(UsersDBDataset::class); + $user = $users->getByEmail($json["email"]); $token = BaseRepository::getUuid(); $code = rand(10000, 99999); @@ -167,8 +163,8 @@ public function postResetRequest(HttpResponse $response, HttpRequest $request) $users->save($user); // Send email using MailWrapper - $mailWrapper = Psr11::container()->get(MailWrapperInterface::class); - $envelope = Psr11::container()->get('MAIL_ENVELOPE', [$json->email, "RestReferenceArchitecture - Password Reset", "email_code.html", [ + $mailWrapper = Psr11::get(MailWrapperInterface::class); + $envelope = Psr11::get('MAIL_ENVELOPE', [$json["email"], "RestReferenceArchitecture - Password Reset", "email_code.html", [ "code" => trim(chunk_split($code, 1, ' ')), "expire" => 10 ]]); @@ -181,18 +177,16 @@ public function postResetRequest(HttpResponse $response, HttpRequest $request) protected function validateResetToken($response, $request): array { - OpenApiContext::validateRequest($request); - - $json = json_decode($request->payload()); + $json = OpenApiContext::validateRequest($request); - $users = Psr11::container()->get(UsersDBDataset::class); - $user = $users->getByEmail($json->email); + $users = Psr11::get(UsersDBDataset::class); + $user = $users->getByEmail($json["email"]); if (is_null($user)) { throw new Error422Exception("Invalid data"); } - if ($user->get("resettoken") != $json->token) { + if ($user->get("resettoken") != $json["token"]) { throw new Error422Exception("Invalid data"); } @@ -241,14 +235,14 @@ public function postConfirmCode(HttpResponse $response, HttpRequest $request) { list($users, $user, $json) = $this->validateResetToken($response, $request); - if ($user->get("resetcode") != $json->code) { + if ($user->get("resetcode") != $json["code"]) { throw new Error422Exception("Invalid data"); } $user->set("resetallowed", "yes"); $users->save($user); - $response->write(['token' => $json->token]); + $response->write(['token' => $json["token"]]); } /** @@ -293,13 +287,13 @@ public function postResetPassword(HttpResponse $response, HttpRequest $request) throw new Error422Exception("Invalid data"); } - $user->setPassword($json->password); + $user->setPassword($json["password"]); $user->set("resettoken", null); $user->set("resettokenexpire", null); $user->set("resetcode", null); $user->set("resetallowed", null); $users->save($user); - $response->write(['token' => $json->token]); + $response->write(['token' => $json["token"]]); } } diff --git a/src/Util/FakeApiRequester.php b/src/Util/FakeApiRequester.php index e828366..ba28271 100644 --- a/src/Util/FakeApiRequester.php +++ b/src/Util/FakeApiRequester.php @@ -9,12 +9,16 @@ use ByJG\Config\Exception\DependencyInjectionException; use ByJG\Config\Exception\InvalidDateException; use ByJG\Config\Exception\KeyNotFoundException; +use ByJG\RestServer\Exception\ClassNotFoundException; +use ByJG\RestServer\Exception\Error404Exception; +use ByJG\RestServer\Exception\Error405Exception; +use ByJG\RestServer\Exception\Error520Exception; +use ByJG\RestServer\Exception\InvalidClassException; use ByJG\RestServer\Middleware\JwtMiddleware; use ByJG\RestServer\MockRequestHandler; use ByJG\RestServer\Route\OpenApiRouteList; -use ByJG\Util\Exception\MessageException; -use ByJG\Util\MockClient; -use ByJG\Util\Psr7\Response; +use ByJG\WebRequest\Exception\RequestException; +use ByJG\WebRequest\MockClient; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; @@ -30,22 +34,27 @@ class FakeApiRequester extends AbstractRequester { /** * @param RequestInterface $request - * @return Response|ResponseInterface + * @return ResponseInterface + * @throws ClassNotFoundException * @throws ConfigException * @throws ConfigNotFoundException * @throws DependencyInjectionException + * @throws Error404Exception + * @throws Error405Exception + * @throws Error520Exception * @throws InvalidArgumentException + * @throws InvalidClassException * @throws InvalidDateException * @throws KeyNotFoundException * @throws ReflectionException - * @throws MessageException + * @throws RequestException */ - protected function handleRequest(RequestInterface $request) + protected function handleRequest(RequestInterface $request): ResponseInterface { - $mock = new MockRequestHandler(Psr11::container()->get(LoggerInterface::class)); - $mock->withMiddleware(Psr11::container()->get(JwtMiddleware::class)); + $mock = new MockRequestHandler(Psr11::get(LoggerInterface::class)); + $mock->withMiddleware(Psr11::get(JwtMiddleware::class)); $mock->withRequestObject($request); - $mock->handle(Psr11::container()->get(OpenApiRouteList::class), false, false); + $mock->handle(Psr11::get(OpenApiRouteList::class), false, false); $httpClient = new MockClient($mock->getPsr7Response()); return $httpClient->sendRequest($request); diff --git a/src/Util/HexUuidLiteral.php b/src/Util/HexUuidLiteral.php deleted file mode 100644 index cfb1af3..0000000 --- a/src/Util/HexUuidLiteral.php +++ /dev/null @@ -1,49 +0,0 @@ -__toString()); - } - - if (preg_match("/^X'(.*)'$/", $item, $matches)) { - $item = $matches[1]; - } - - if (is_string($item) && !ctype_print($item) && strlen($item) === 16) { - $item = bin2hex($item); - } - - if (preg_match("/^\w{8}-?\w{4}-?\w{4}-?\w{4}-?\w{12}$/", $item)) { - $item = preg_replace("/^(\w{8})-?(\w{4})-?(\w{4})-?(\w{4})-?(\w{12})$/", "$1-$2-$3-$4-$5", $item); - } else if ($throwErrorIfInvalid) { - throw new InvalidArgumentException("Invalid UUID format"); - } else { - return $item; - } - - return strtoupper($item); - } -} diff --git a/src/Util/HexUuidMysqlLiteral.php b/src/Util/HexUuidMysqlLiteral.php deleted file mode 100644 index 80ca784..0000000 --- a/src/Util/HexUuidMysqlLiteral.php +++ /dev/null @@ -1,13 +0,0 @@ -get(JwtWrapper::class); + $jwt = Psr11::get(JwtWrapper::class); $jwtData = $jwt->createJwtData($properties, 60 * 60 * 24 * 7); // 7 Dias return $jwt->generateToken($jwtData); } diff --git a/src/Util/OpenApiContext.php b/src/Util/OpenApiContext.php index 4766791..49f97d5 100644 --- a/src/Util/OpenApiContext.php +++ b/src/Util/OpenApiContext.php @@ -3,25 +3,40 @@ namespace RestReferenceArchitecture\Util; use ByJG\ApiTools\Base\Schema; +use ByJG\Config\Exception\ConfigException; +use ByJG\Config\Exception\ConfigNotFoundException; +use ByJG\Config\Exception\DependencyInjectionException; +use ByJG\Config\Exception\InvalidDateException; +use ByJG\Config\Exception\KeyNotFoundException; use ByJG\RestServer\Exception\Error400Exception; use ByJG\RestServer\HttpRequest; +use ByJG\Serializer\Serialize; use Exception; +use Psr\SimpleCache\InvalidArgumentException; +use ReflectionException; use RestReferenceArchitecture\Psr11; class OpenApiContext { - public static function validateRequest(HttpRequest $request) + /** + * @throws DependencyInjectionException + * @throws InvalidDateException + * @throws ConfigNotFoundException + * @throws KeyNotFoundException + * @throws Error400Exception + * @throws InvalidArgumentException + * @throws ConfigException + * @throws ReflectionException + */ + public static function validateRequest(HttpRequest $request, bool $allowNull = false) { - $schema = Psr11::container()->get(Schema::class); + $schema = Psr11::get(Schema::class); $path = $request->getRequestPath(); $method = $request->server('REQUEST_METHOD'); - // Returns a SwaggerRequestBody instance - $bodyRequestDef = $schema->getRequestParameters($path, $method); - // Validate the request body (payload) - if (str_contains($request->getHeader('Content-Type'), 'multipart/')) { + if (str_contains($request->getHeader('Content-Type') ?? "", 'multipart/')) { $requestBody = $request->post(); $files = $request->uploadedFiles()->getKeys(); $requestBody = array_merge($requestBody, array_combine($files, $files)); @@ -30,11 +45,22 @@ public static function validateRequest(HttpRequest $request) } try { + // Validate the request path and query against the OpenAPI schema + $schema->getPathDefinition($path, $method); + // Returns a SwaggerRequestBody instance + $bodyRequestDef = $schema->getRequestParameters($path, $method); $bodyRequestDef->match($requestBody); + } catch (Exception $ex) { throw new Error400Exception(explode("\n", $ex->getMessage())[0]); } - return $requestBody; + $requestBody = empty($requestBody) ? [] : $requestBody; + + if ($allowNull) { + return $requestBody; + } + + return Serialize::from($requestBody)->withDoNotParseNullValues()->toArray(); } -} \ No newline at end of file +} diff --git a/templates/codegen/model.php.jinja b/templates/codegen/model.php.jinja index 5e26588..b7a155b 100644 --- a/templates/codegen/model.php.jinja +++ b/templates/codegen/model.php.jinja @@ -1,6 +1,12 @@ 0 %}, "{{ nonNullableFields | join('", "')}}"{% endif %}], type: "object", xml: new OA\Xml(name: "{{ className }}"))] +#[Table{% if autoIncrement == "no" %}MySqlUuidPK{% endif %}Attribute("{{ tableName }}")] class {{ className }} { {% for field in fields %} @@ -15,23 +22,24 @@ class {{ className }} * @var {{ field.php_type }}|null */ #[OA\Property(type: "{{ field.openapi_type }}", format: "{{ field.openapi_format }}"{% if field.null == "YES" %}, nullable: true{% endif %})] - protected ?{{ field.php_type }} ${{ field.property }} = null; + #[Field{% if 'binary' in field.type %}Uuid{% endif %}Attribute({% if field.key == "PRI" %}primaryKey: true, {% endif %}fieldName: "{{ field.field }}"{% if 'VIRTUAL' in field.extra %}, syncWithDb: false{% endif %})] + protected {{ field.php_type }}{% if 'binary' in field.type %}|HexUuidLiteral{% endif %}|null ${{ field.property }} = null; {% endfor %} {% for field in fields %} /** - * @return {{ field.php_type }}|null + * @return {{ field.php_type }}{% if 'binary' in field.type %}|HexUuidLiteral{% endif %}|null */ - public function get{{ field.property | capitalize }}(): ?{{ field.php_type }} + public function get{{ field.property | capitalize }}(): {{ field.php_type }}{% if 'binary' in field.type %}|HexUuidLiteral{% endif %}|null { return $this->{{ field.property }}; } /** - * @param {{ field.php_type }}|null ${{ field.property }} - * @return {{ className }} + * @param {{ field.php_type }}{% if 'binary' in field.type %}|HexUuidLiteral{% endif %}|null ${{ field.property }} + * @return $this */ - public function set{{ field.property | capitalize }}(?{{ field.php_type }} ${{ field.property }}): {{ className }} + public function set{{ field.property | capitalize }}({{ field.php_type }}{% if 'binary' in field.type %}|HexUuidLiteral{% endif %}|null ${{ field.property }}): static { $this->{{ field.property }} = ${{ field.property }}; return $this; diff --git a/templates/codegen/repository.php.jinja b/templates/codegen/repository.php.jinja index 1b4ca6a..c764417 100644 --- a/templates/codegen/repository.php.jinja +++ b/templates/codegen/repository.php.jinja @@ -3,9 +3,9 @@ namespace {{ namespace }}\Repository; use {{ namespace }}\Psr11; +use ByJG\MicroOrm\Exception\OrmModelInvalidException; +use ReflectionException; use ByJG\AnyDataset\Db\DbDriverInterface; -use ByJG\MicroOrm\FieldMapping; -use ByJG\MicroOrm\Mapper; use ByJG\MicroOrm\Query; use ByJG\MicroOrm\Repository; use {{ namespace }}\Model\{{ className }}; @@ -16,29 +16,12 @@ class {{ className }}Repository extends BaseRepository * {{ className }}Repository constructor. * * @param DbDriverInterface $dbDriver - * + * @throws OrmModelInvalidException + * @throws ReflectionException */ public function __construct(DbDriverInterface $dbDriver) { - $mapper = new Mapper( - {{ className }}::class, - '{{ tableName }}', - 'id' - ); - // $mapper->withPrimaryKeySeedFunction(BaseRepository::getClosureNewUUID()); - - - // Table UUID Definition - // $this->setClosureFixBinaryUUID($mapper); -{% for field in fields -%} -{% if 'GENERATED' in field.extra -%} - $mapper->addFieldMapping(FieldMapping::create('{{ field.property }}')->withFieldName('{{ field.field }}')->withUpdateFunction(Mapper::doNotUpdateClosure())); -{% else %}{% if field.property != field.field -%} - $mapper->addFieldMapping(FieldMapping::create('{{ field.property }}')->withFieldName('{{ field.field }}')); -{% endif %}{% endif %} -{% endfor %} - - $this->repository = new Repository($dbDriver, $mapper); + $this->repository = new Repository($dbDriver, {{ className }}::class); } {% for index in indexes -%} {% if index.key_name != 'PRIMARY' -%} diff --git a/templates/codegen/rest.php.jinja b/templates/codegen/rest.php.jinja index 27f5d96..22ef5ac 100644 --- a/templates/codegen/rest.php.jinja +++ b/templates/codegen/rest.php.jinja @@ -16,7 +16,7 @@ use ByJG\RestServer\Exception\Error403Exception; use ByJG\RestServer\Exception\Error404Exception; use ByJG\RestServer\HttpRequest; use ByJG\RestServer\HttpResponse; -use ByJG\Serializer\BinderObject; +use ByJG\Serializer\ObjectCopy; use OpenApi\Attributes as OA; use ReflectionException; use {{ namespace }}\Model\{{ className }}; @@ -70,7 +70,7 @@ class {{ className }}Rest { JwtContext::requireAuthenticated($request); - ${{ varTableName }}Repo = Psr11::container()->get({{ className }}Repository::class); + ${{ varTableName }}Repo = Psr11::get({{ className }}Repository::class); $id = $request->param('id'); $result = ${{ varTableName }}Repo->get($id); @@ -156,7 +156,7 @@ class {{ className }}Rest { JwtContext::requireAuthenticated($request); - $repo = Psr11::container()->get({{ className }}Repository::class); + $repo = Psr11::get({{ className }}Repository::class); $page = $request->get('page'); $size = $request->get('size'); @@ -234,9 +234,9 @@ class {{ className }}Rest $payload = OpenApiContext::validateRequest($request); $model = new {{ className }}(); - BinderObject::bind($payload, $model); + ObjectCopy::copy($payload, $model); - ${{ varTableName }}Repo = Psr11::container()->get({{ className }}Repository::class); + ${{ varTableName }}Repo = Psr11::get({{ className }}Repository::class); ${{ varTableName }}Repo->save($model); $response->write([ "id" => $model->getId()]); @@ -292,12 +292,12 @@ class {{ className }}Rest $payload = OpenApiContext::validateRequest($request); - ${{ varTableName }}Repo = Psr11::container()->get({{ className }}Repository::class); + ${{ varTableName }}Repo = Psr11::get({{ className }}Repository::class); $model = ${{ varTableName }}Repo->get($payload['{{ fields.0.field }}']); if (empty($model)) { throw new Error404Exception('Id not found'); } - BinderObject::bind($payload, $model); + ObjectCopy::copy($payload, $model); ${{ varTableName }}Repo->save($model); } diff --git a/templates/codegen/test.php.jinja b/templates/codegen/test.php.jinja index ab21122..a44ef8f 100644 --- a/templates/codegen/test.php.jinja +++ b/templates/codegen/test.php.jinja @@ -1,10 +1,10 @@ withScheme(Psr11::container()->get("API_SCHEMA")) - ->withHost(Psr11::container()->get("API_SERVER")); + ->withScheme(Psr11::get("API_SCHEMA")) + ->withHost(Psr11::get("API_SERVER")); return Request::getInstance($uri); } @@ -44,7 +44,7 @@ public function resetDb() throw new Exception("This test can only be executed in test environment"); } Migration::registerDatabase(MySqlDatabase::class); - $migration = new Migration(new Uri(Psr11::container()->get('DBDRIVER_CONNECTION')), __DIR__ . "/../../../db"); + $migration = new Migration(new Uri(Psr11::get('DBDRIVER_CONNECTION')), __DIR__ . "/../../../db"); $migration->prepareEnvironment(); $migration->reset(); self::$databaseReset = true; diff --git a/tests/Functional/Rest/Credentials.php b/tests/Rest/Credentials.php similarity index 86% rename from tests/Functional/Rest/Credentials.php rename to tests/Rest/Credentials.php index 13c966b..3444688 100644 --- a/tests/Functional/Rest/Credentials.php +++ b/tests/Rest/Credentials.php @@ -1,9 +1,9 @@ withScheme(Psr11::container()->get("API_SCHEMA")) - ->withHost(Psr11::container()->get("API_SERVER")); + ->withScheme(Psr11::get("API_SCHEMA")) + ->withHost(Psr11::get("API_SERVER")); $psr7Request = Request::getInstance($uri); diff --git a/tests/Functional/Rest/DummyHexTest.php b/tests/Rest/DummyHexTest.php similarity index 98% rename from tests/Functional/Rest/DummyHexTest.php rename to tests/Rest/DummyHexTest.php index 0cf34ac..2473795 100644 --- a/tests/Functional/Rest/DummyHexTest.php +++ b/tests/Rest/DummyHexTest.php @@ -1,10 +1,10 @@ get(UsersDBDataset::class); + $userRepo = Psr11::get(UsersDBDataset::class); $user = $userRepo->getByEmail($email); $user->set(User::PROP_RESETTOKEN, null); $user->set(User::PROP_RESETTOKENEXPIRE, null); @@ -69,7 +69,7 @@ public function testResetRequestOk() $this->assertRequest($request); // Check if the reset token was created - $userRepo = Psr11::container()->get(UsersDBDataset::class); + $userRepo = Psr11::get(UsersDBDataset::class); $user = $userRepo->getByEmail($email); $this->assertNotNull($user); $this->assertNotEmpty($user->get(User::PROP_RESETTOKEN)); @@ -83,7 +83,7 @@ public function testConfirmCodeFail() $email = Credentials::getRegularUser()["username"]; // Clear the reset token - $userRepo = Psr11::container()->get(UsersDBDataset::class); + $userRepo = Psr11::get(UsersDBDataset::class); $user = $userRepo->getByEmail($email); $this->assertNotNull($user); $this->assertNotEmpty($user->get(User::PROP_RESETTOKEN)); @@ -110,7 +110,7 @@ public function testConfirmCodeOk() $email = Credentials::getRegularUser()["username"]; // Clear the reset token - $userRepo = Psr11::container()->get(UsersDBDataset::class); + $userRepo = Psr11::get(UsersDBDataset::class); $user = $userRepo->getByEmail($email); $this->assertNotNull($user); $this->assertNotEmpty($user->get(User::PROP_RESETTOKEN)); @@ -144,7 +144,7 @@ public function testPasswordResetOk() $password = Credentials::getRegularUser()["password"]; // Clear the reset token - $userRepo = Psr11::container()->get(UsersDBDataset::class); + $userRepo = Psr11::get(UsersDBDataset::class); $user = $userRepo->getByEmail($email); $this->assertNotNull($user); $this->assertNotEmpty($user->get(User::PROP_RESETTOKEN)); diff --git a/tests/Functional/Rest/SampleProtectedTest.php b/tests/Rest/SampleProtectedTest.php similarity index 98% rename from tests/Functional/Rest/SampleProtectedTest.php rename to tests/Rest/SampleProtectedTest.php index c9f3c89..d6294ec 100644 --- a/tests/Functional/Rest/SampleProtectedTest.php +++ b/tests/Rest/SampleProtectedTest.php @@ -1,6 +1,6 @@