diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fa5f5077..2c99dc37 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,13 +7,14 @@ env: FTP_USER: username FTP_PASSWORD: password FTP_PORT: 21 + FTP_ROOT: /tmp jobs: lunix-tests: runs-on: ${{ matrix.os }} strategy: matrix: - php: [8.1, 8.2] + php: [8.1, 8.2, 8.3] os: [ubuntu-latest] stability: [prefer-lowest, prefer-stable] @@ -41,11 +42,11 @@ jobs: extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, mysql, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, redis coverage: none - - run: docker run --net=host -p 21:21 -e USER=$FTP_USER -e PASS=$FTP_PASSWORD -d --name ftp -v $(pwd)/:/ftp/$FTP_USER emilybache/vsftpd-server + - run: docker run -p 21:21 -p 20:20 -p 12020:12020 -p 12021:12021 -p 12022:12022 -p 12023:12023 -p 12024:12024 -p 12025:12025 -e USER=$FTP_USER -e PASS=$FTP_PASSWORD -d --name ftp papacdev/vsftpd - run: docker run -p 1080:1080 -p 1025:1025 -d --name maildev soulteary/maildev - run: docker run -p 6379:6379 -d --name redis redis - run: docker run -p 5432:5432 --name postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=postgres -e POSTGRES_PASSWORD=postgres -d postgis/postgis - - run: docker run -d -p 11301:11300 schickling/beanstalkd + - run: docker run -d -p 11300:11300 schickling/beanstalkd - name: Cache Composer packages id: composer-cache @@ -57,13 +58,13 @@ jobs: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - name: Copy the php ini config - run: cp php.dist.ini php.ini + run: sudo cp php.dist.ini php.ini - name: Install dependencies - run: composer update --prefer-dist --no-interaction + run: sudo composer update --prefer-dist --no-interaction - name: Create test cache directory run: if [ ! -d /tmp/bowphp_testing ]; then mkdir -p /tmp/bowphp_testing; fi; - name: Run test suite - run: composer run-script test + run: sudo composer run-script test diff --git a/CHANGELOG.md b/CHANGELOG.md index 03d74dff..743cac52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,134 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## 5.1.7 - 2024-12-21 + +### What's Changed + +* Update CHANGELOG by @papac in https://github.com/bowphp/framework/pull/305 +* feat(barry): add relative create method for barry model by @papac in https://github.com/bowphp/framework/pull/306 + +**Full Changelog**: https://github.com/bowphp/framework/compare/5.1.6...5.1.7 + +## 5.1.6 - 2024-12-20 + +### What's Changed + +* Implement feature for improve http and str classes by @papac in https://github.com/bowphp/framework/pull/304 + +**Full Changelog**: https://github.com/bowphp/framework/compare/5.1.5...5.1.6 + +## 5.1.2 - 2023-09-17 + +Fix `db_seed` helper + +Ref + +- #257 +- #256 + +## 5.1.1 - 2023-08-21 + +Add the transaction method + +This method aims to execute an SQL transaction around a passed arrow function. + +```php +Database::transaction(fn() => $user->update(['name' => ''])); + + + + +``` +Ref: #255 + +## 5.1.0 - 2023-06-07 + +Release 5.1.0 + +- Add custom adaptor #252 +- Make Redis accessible on whole project #250 + +## 5.0.9 - 2023-06-01 + +Release 5.0.9 + +Fixes many bugs + +Reference #248 + +## 5.0.8 - 2023-05-24 + +Release 5.0.8 + +Fixes test case errors + +Reference #243 +From #242 + +## 5.0.7 - 2023-05-24 + +Release 5.0.7 + +- Fixes the database relationship +- Fixes the HTTP client +- Fixes the JWT authentication service + +Fixes #241 +Fixes #213 +Fixes #240 + +## 5.0.6 - 2023-05-22 + +Release 5.0.6 + +- Fixes get last insert id for pgsql +- Add data validation custom message parser +- Fixes PgSQL migration errors +- Fixes initialize the request ID #236 + +References + +- Validation and PgSQL #237 +- Many bugs fixes #237 + +## 5.0.5 - 2023-05-20 + +Release 5.0.5 + +- Fix migration status table definition +- Fix enum creation for pgsql + +Reference #232 + +## 5.0.4 - 2023-05-19 + +Release 5.0.4 + +- Fixes HTTP Client +- Add variable binding to the env loader +- Fixes validation for regex rule +- Fixes request data parser +- Fixes middleware execution order + +All update ref #230 + +## 5.0.3 - 2023-05-16 + +Add many fixes + +- Fixes the error handler +- Fixes the HTTP client +- Fixes TestCase service + +## 5.0.2 - 2023-05-16 + +Release for 5.0.2 + +- Fix action dependency injector +- Add the base error handler + +## 5.0.0 - 2023-05-10 - [Add] Convert the project from PHP7 to PHP8 - [Add] Multiconnection for storage system diff --git a/composer.json b/composer.json index b2959369..a64fd6ab 100644 --- a/composer.json +++ b/composer.json @@ -12,29 +12,21 @@ "bowphp/tintin": "^3.0", "filp/whoops": "^2.1", "nesbot/carbon": "^2.16", - "psy/psysh": "v0.10.*", + "psy/psysh": "v0.12.*", "fakerphp/faker": "^1.20", "neitanod/forceutf8": "^2.0", - "ext-openssl": "*", - "ext-intl": "*", - "ext-pdo": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-json": "*", - "ext-pdo_mysql": "*", - "ext-pdo_sqlite": "*", - "ext-redis": "*" + "ramsey/uuid": "^4.7" }, "require-dev": { - "pda/pheanstalk": "^4.0", - "phpunit/phpunit": "^8", + "pda/pheanstalk": "^4.0.5", + "phpunit/phpunit": "^9", "monolog/monolog": "^1.22", - "twig/twig": "^2.5", + "twig/twig": "^3", "squizlabs/php_codesniffer": "3.*", "aws/aws-sdk-php": "^3.87", "phpstan/phpstan": "^0.12.87", "php-amqplib/php-amqplib": "^3.0", - "bowphp/policier": "dev-master", + "bowphp/policier": "^3.0", "mockery/mockery": "^1.5", "spatie/phpunit-snapshot-assertions": "^4.2", "predis/predis": "^2.1" diff --git a/readme.md b/readme.md index 027fc11f..f60325f2 100644 --- a/readme.md +++ b/readme.md @@ -8,7 +8,7 @@ To use this package, please create an application from this package [bowphp/app] ## The Framework Main Feature -- Full Featured database classes with support for several platforms. +- Full-featured database classes with support for several platforms. - Query Builder Database Support - Form and Data Validation - Security and XSS Filtering @@ -21,10 +21,10 @@ To use this package, please create an application from this package [bowphp/app] - Pagination - CQRS helpful implementation - File System Management with many drivers like S3 and FTP (Support connection switch) -- Extensible with an external package that can plug-in +- Extensible with an external package that can plug in - Application logs Management - Database Connection (MySQL, SQLite, PostgreSQL) -- Simplest ORM which names Barry +- Simplest ORM which is named Barry - Cache support (Filesystem, Redis, Database caching) - Event Management (Interpage Event) - Emailing (SMTP, SES, Native PHP mail supports) @@ -34,6 +34,7 @@ To use this package, please create an application from this package [bowphp/app] - Very easy Translate Management - Many helpers - The native authentication system +- Producer/Consumer with beanstalkd, database, Redis, SQS backend ## Contributing diff --git a/src/Application/Application.php b/src/Application/Application.php index 05987e3c..50541f31 100644 --- a/src/Application/Application.php +++ b/src/Application/Application.php @@ -82,10 +82,11 @@ public function __construct(Request $request, Response $response) $this->capsule = Capsule::getInstance(); - $this->capsule->instance('request', $request); $this->capsule->instance('response', $response); + $this->capsule->instance('request', $request); $this->capsule->instance('app', $this); + $this->request->capture(); parent::__construct($request->method(), $request->get('_method')); } @@ -195,30 +196,17 @@ public function send(): ?bool // the routing collection $routes = $this->getRoutes(); - if (!isset($routes[$method])) { - // We verify and call function associate by 404 code - $this->response->status(404); - - if (empty($this->error_code)) { - $this->response->send( - sprintf('Cannot %s %s 404', $method, $this->request->path()) - ); - } - - return false; - } - $response = null; $resolved = false; - foreach ($routes[$method] as $route) { + foreach ($routes[$method] ?? [] as $route) { // The route must be an instance of Route if (!($route instanceof Route)) { continue; } // We launch the search of the method that arrived in the query - // then start checking the url of the request + // then start checking the url of the request if (!$route->match($this->request->path())) { continue; } diff --git a/src/Application/Exception/BaseErrorHandler.php b/src/Application/Exception/BaseErrorHandler.php new file mode 100644 index 00000000..b1c475f1 --- /dev/null +++ b/src/Application/Exception/BaseErrorHandler.php @@ -0,0 +1,83 @@ +getContent(); + } + + /** + * Send the json as response + * + * @param string $data + * @param mixed $code + * @return mixed + */ + protected function json($exception, $code = null) + { + if ($exception instanceof TokenInvalidException) { + $code = 'TOKEN_INVALID'; + } + + if ($exception instanceof TokenExpiredException) { + $code = 'TOKEN_EXPIRED'; + } + + if (is_null($code)) { + if (method_exists($exception, 'getStatus')) { + $code = $exception->getStatus(); + } else { + $code = 'INTERNAL_SERVER_ERROR'; + } + } + + if (app_env("APP_ENV") == "production" && $exception instanceof PDOException) { + $message = 'An SQL error occurs. For security, we did not display the message.'; + } else { + $message = $exception->getMessage(); + } + + $response = [ + 'message' => $message, + 'code' => $code, + 'time' => date('Y-m-d H:i:s') + ]; + + $status = 500; + + if ($exception instanceof HttpException) { + $status = $exception->getStatusCode(); + $response = array_merge($response, compact('status')); + if ($exception instanceof ValidationException) { + $response["errors"] = $exception->getErrors(); + } + } + + if (app_env("APP_ENV") != "production") { + $response["trace"] = $exception->getTrace(); + } + + response()->status($status); + + return die(json_encode($response)); + } +} diff --git a/src/Auth/Auth.php b/src/Auth/Auth.php index d8323df9..190e090a 100644 --- a/src/Auth/Auth.php +++ b/src/Auth/Auth.php @@ -8,6 +8,7 @@ use Bow\Auth\Guards\SessionGuard; use Bow\Auth\Guards\GuardContract; use Bow\Auth\Exception\AuthenticationException; +use ErrorException; class Auth { @@ -100,10 +101,16 @@ public static function guard(?string $guard = null): GuardContract * * @param string $method * @param array $params - * @return GuardContract + * @return ?GuardContract */ public static function __callStatic(string $method, array $params) { + if (is_null(static::$instance)) { + throw new ErrorException( + "Unable to get auth instance before configuration" + ); + } + if (method_exists(static::$instance, $method)) { return call_user_func_array([static::$instance, $method], $params); } diff --git a/src/Auth/Authentication.php b/src/Auth/Authentication.php index 6ada4fd1..4dbec6c1 100644 --- a/src/Auth/Authentication.php +++ b/src/Auth/Authentication.php @@ -17,4 +17,14 @@ public function getAuthenticateUserId() { return $this->attributes[$this->primary_key]; } + + /** + * Define the additionals values + * + * @return array + */ + public function customJwtAttributes(): array + { + return []; + } } diff --git a/src/Auth/Guards/GuardContract.php b/src/Auth/Guards/GuardContract.php index 36f91fe5..57e0cf25 100644 --- a/src/Auth/Guards/GuardContract.php +++ b/src/Auth/Guards/GuardContract.php @@ -8,7 +8,7 @@ use Bow\Auth\Authentication; /** - * @method ?string getToken() + * @method ?\Policier\Token getToken() */ abstract class GuardContract { diff --git a/src/Auth/Guards/JwtGuard.php b/src/Auth/Guards/JwtGuard.php index 2d2e5a4a..4edf0309 100644 --- a/src/Auth/Guards/JwtGuard.php +++ b/src/Auth/Guards/JwtGuard.php @@ -80,6 +80,16 @@ public function attempts(array $credentials): bool */ public function check(): bool { + $policier = $this->getPolicier(); + + if (is_null($this->token)) { + try { + $this->token = $policier->getParsedToken(); + } catch (\Exception $e) { + return false; + } + } + if (is_null($this->token)) { return false; } @@ -147,11 +157,13 @@ public function getToken(): ?Token */ public function login(Authentication $user): bool { - $this->token = $this->getPolicier()->encode($user->getAuthenticateUserId(), [ + $attributes = array_merge($user->customJwtAttributes(), [ "id" => $user->getAuthenticateUserId(), "logged" => true ]); + $this->token = $this->getPolicier()->encode($user->getAuthenticateUserId(), $attributes); + return true; } diff --git a/src/Auth/Guards/SessionGuard.php b/src/Auth/Guards/SessionGuard.php index 68e58004..5114ba1d 100644 --- a/src/Auth/Guards/SessionGuard.php +++ b/src/Auth/Guards/SessionGuard.php @@ -78,7 +78,9 @@ private function getSession(): Session $session = Session::getInstance(); if (is_null($session)) { - throw new AuthenticationException("Please the session configuration is not load"); + throw new AuthenticationException( + "Please the session configuration is not load" + ); } return $session; diff --git a/src/Cache/Adapter/FilesystemAdapter.php b/src/Cache/Adapter/FilesystemAdapter.php index 649eb158..99ff1ec9 100644 --- a/src/Cache/Adapter/FilesystemAdapter.php +++ b/src/Cache/Adapter/FilesystemAdapter.php @@ -43,7 +43,7 @@ public function __construct(array $config) /** * @inheritDoc */ - public function add(string $key, mixed $data, ?int $time = null): bool + public function add(string $key, mixed $data, ?int $time = 60): bool { if (is_callable($data)) { $content = $data(); @@ -51,7 +51,7 @@ public function add(string $key, mixed $data, ?int $time = null): bool $content = $data; } - $meta['__bow_meta'] = ['expire_at' => $time == null ? '+' : $time]; + $meta['__bow_meta'] = ['expire_at' => $time == null ? '+' : time() + $time]; $meta['content'] = $content; @@ -142,6 +142,14 @@ public function get(string $key, mixed $default = null): mixed $cache = unserialize(file_get_contents($this->makeHashFilename($key))); + $expire_at = $cache['__bow_meta']['expire_at']; + + if ($expire_at != '+') { + if (time() > $expire_at) { + return null; + } + } + if (!$this->with_meta) { unset($cache['__bow_meta']); @@ -167,11 +175,11 @@ public function addTime(string $key, int $time): bool } if ($cache['__bow_meta']['expire_at'] == '+') { - $cache['__bow_meta']['expire_at'] = time(); + $cache['__bow_meta']['expire_at'] = time() + $time; + } else { + $cache['__bow_meta']['expire_at'] += $time; } - $cache['__bow_meta']['expire_at'] += $time; - return (bool) file_put_contents( $this->makeHashFilename($key), serialize($cache) @@ -193,7 +201,7 @@ public function timeOf(string $key): int|bool|string return false; } - return $cache['__bow_meta']['expire_at']; + return (int) $cache['__bow_meta']['expire_at']; } /** @@ -243,11 +251,11 @@ public function expired(string $key): bool return false; } + $expire_at = $cache['__bow_meta']['expire_at']; + $this->with_meta = false; - return $cache['__bow_meta']['expire_at'] == '+' - ? false - : (time() > $cache['__bow_meta']['expire_at']); + return $expire_at == '+' ? false : (time() > $expire_at); } /** diff --git a/src/Cache/Adapter/RedisAdapter.php b/src/Cache/Adapter/RedisAdapter.php index dfaa92c0..f076f678 100644 --- a/src/Cache/Adapter/RedisAdapter.php +++ b/src/Cache/Adapter/RedisAdapter.php @@ -4,6 +4,7 @@ use Redis; use Bow\Cache\Adapter\CacheAdapterInterface; +use Bow\Database\Redis as RedisStore; class RedisAdapter implements CacheAdapterInterface { @@ -22,39 +23,7 @@ class RedisAdapter implements CacheAdapterInterface */ public function __construct(array $config) { - - $options = []; - $auth = []; - if (isset($config["password"])) { - $auth[] = $config["password"]; - } - - if (isset($config["username"]) && !is_null($config["username"])) { - array_unshift($auth, $config["username"]); - } - - if (count($auth) > 0) { - $options = compact('auth'); - } - - $options['backoff'] = [ - 'algorithm' => Redis::BACKOFF_ALGORITHM_DECORRELATED_JITTER, - 'base' => 500, - 'cap' => 750, - ]; - - $this->redis = new Redis(); - $this->redis->connect( - $config["host"], - $config["port"] ?? 6379, - $config["timeout"] ?? 2.5, - null, - 0, - 0, - $options - ); - - $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_JSON); + $this->redis = RedisStore::getClient(); if (isset($config["prefix"])) { $this->redis->setOption(Redis::OPT_PREFIX, $config["prefix"]); diff --git a/src/Cache/Cache.php b/src/Cache/Cache.php index 87c77e99..bf4a4514 100644 --- a/src/Cache/Cache.php +++ b/src/Cache/Cache.php @@ -42,9 +42,9 @@ class Cache /** * Cache configuration method * - * @param string $base_directory + * @param array $config */ - public static function confirgure(array $config) + public static function configure(array $config) { if (!is_null(static::$instance)) { return static::$instance; @@ -57,7 +57,7 @@ public static function confirgure(array $config) static::$config = $config; $store = (array) $config["stores"][$config["default"]]; - return static::cache($store["driver"]); + return static::store($store["driver"]); } /** @@ -80,7 +80,7 @@ public static function getInstance(): CacheAdapterInterface * @param string $driver * @return CacheAdapterInterface */ - public static function cache(string $store): CacheAdapterInterface + public static function store(string $store): CacheAdapterInterface { $stores = static::$config["stores"]; @@ -95,6 +95,19 @@ public static function cache(string $store): CacheAdapterInterface return static::$instance; } + /** + * Add the custom adapters + * + * @param array $adapters + * @return void + */ + public static function addAdapters(array $adapters): void + { + foreach ($adapters as $name => $adapter) { + static::$adapters[$name] = $adapter; + } + } + /** * __call * @@ -102,13 +115,22 @@ public static function cache(string $store): CacheAdapterInterface * @param array $arguments * @return mixed * @throws BadMethodCallException + * @throws ErrorException */ public static function __callStatic(string $name, array $arguments) { + if (is_null(static::$instance)) { + throw new ErrorException( + "Unable to get cache instance before configuration" + ); + } + if (method_exists(static::$instance, $name)) { return call_user_func_array([static::$instance, $name], $arguments); } - throw new BadMethodCallException("The $name method does not exist"); + throw new BadMethodCallException( + "The $name method does not exist" + ); } } diff --git a/src/Cache/CacheConfiguration.php b/src/Cache/CacheConfiguration.php index 6383fda3..e85650a7 100644 --- a/src/Cache/CacheConfiguration.php +++ b/src/Cache/CacheConfiguration.php @@ -16,7 +16,7 @@ class CacheConfiguration extends Configuration public function create(Loader $config): void { $this->container->bind('cache', function () use ($config) { - return Cache::confirgure($config['cache']); + return Cache::configure($config['cache']); }); } diff --git a/src/Cache/README.md b/src/Cache/README.md index 20f6ad45..5c4e9f0c 100644 --- a/src/Cache/README.md +++ b/src/Cache/README.md @@ -2,6 +2,21 @@ Bow Framework's cache system is very simple cache manager +- Filesystem +- Database +- Redis +- With extended driver interface + +Let's show a little exemple: + ```php $content = cache("name"); ``` + +By specifying the driver: + +``` +$content = Cache::store('redis')->get('name'); +``` + +Is very enjoyful api diff --git a/src/Configuration/Configuration.php b/src/Configuration/Configuration.php index 7c04229c..b3319dd1 100644 --- a/src/Configuration/Configuration.php +++ b/src/Configuration/Configuration.php @@ -23,6 +23,16 @@ public function __construct(Container $container) $this->container = $container; } + /** + * Get the container instance + * + * @return Container + */ + public function getContainer(): Container + { + return $this->container; + } + /** * Get la service class name * diff --git a/src/Configuration/Loader.php b/src/Configuration/Loader.php index 0075fcbd..010afa34 100644 --- a/src/Configuration/Loader.php +++ b/src/Configuration/Loader.php @@ -119,30 +119,6 @@ public static function configure($base_path): Loader return static::$instance; } - /** - * Push middlewares - * - * @param array $middlewares - */ - public function pushMiddleware(array $middlewares): void - { - foreach ($middlewares as $key => $middleware) { - $this->middlewares[$key] = $middleware; - } - } - - /** - * Push namespaces - * - * @param array $namespaces - */ - public function pushNamespaces(array $namespaces): void - { - foreach ($namespaces as $key => $namespace) { - $this->namespaces[$key] = $namespace; - } - } - /** * Middleware collection * diff --git a/src/Configuration/LoggerConfiguration.php b/src/Configuration/LoggerConfiguration.php index b4590c7c..e98f5519 100644 --- a/src/Configuration/LoggerConfiguration.php +++ b/src/Configuration/LoggerConfiguration.php @@ -6,14 +6,17 @@ use Bow\View\View; use Monolog\Logger; -use Bow\Http\Redirect; use Bow\Support\Collection; +use Whoops\Handler\Handler; use Bow\Configuration\Loader; use Bow\Database\Barry\Model; use Monolog\Handler\StreamHandler; use Monolog\Handler\FirePHPHandler; +use Whoops\Handler\CallbackHandler; use Bow\Configuration\Configuration; use Bow\Contracts\ResponseInterface; +use Iterator; +use Whoops\Handler\PrettyPageHandler; class LoggerConfiguration extends Configuration { @@ -54,44 +57,38 @@ private function loadFrontLogger(Logger $monolog, $error_handler) { $whoops = new \Whoops\Run(); - if (app_env('APP_ENV') == 'development') { - $whoops->pushHandler( - new \Whoops\Handler\PrettyPageHandler() - ); + if (app_env('APP_DEBUG')) { + $whoops->pushHandler(new PrettyPageHandler()); + $whoops->register(); + return; } - if (class_exists($error_handler)) { - $handler = new \Whoops\Handler\CallbackHandler( - function ($exception, $inspector, $run) use ($monolog, $error_handler) { - $monolog->error($exception->getMessage(), $exception->getTrace()); - - $result = call_user_func_array( - [new $error_handler(), 'handle'], - [$exception] - ); - - switch (true) { - case $result instanceof View: - return $result->getContent(); - case $result instanceof ResponseInterface || $result instanceof Redirect: - $result->sendContent(); - break; - case $result instanceof Model || $result instanceof Collection: - return $result->toArray(); - case is_null($result): - case is_string($result): - case is_array($result): - case is_object($result): - case $result instanceof \Iterable: - return $result; - } - exit(1); + $handler = new CallbackHandler( + function ($exception, $inspector, $run) use ($monolog, $error_handler) { + $monolog->error($exception->getMessage(), $exception->getTrace()); + + $result = call_user_func_array([new $error_handler(), 'handle'], [$exception]); + + if ($result instanceof View) { + echo $result->getContent(); + } elseif ($result instanceof ResponseInterface) { + $result->sendContent(); + } elseif ( + $result instanceof Model || $result instanceof Collection + || is_array($result) + || is_object($result) + || $result instanceof Iterator + ) { + echo json_encode($result); + } elseif (is_string($result)) { + echo $result; } - ); - $whoops->pushHandler($handler); - } + return Handler::QUIT; + } + ); + $whoops->pushHandler($handler); $whoops->register(); } diff --git a/src/Console/Command.php b/src/Console/Command.php index 8e631f96..a2966093 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -37,6 +37,7 @@ class Command extends AbstractCommand "key" => \Bow\Console\Command\GenerateKeyCommand::class, "resource" => \Bow\Console\Command\GenerateResourceControllerCommand::class, "session" => \Bow\Console\Command\GenerateSessionCommand::class, + "queue" => \Bow\Console\Command\GenerateQueueCommand::class, "cache" => \Bow\Console\Command\GenerateCacheCommand::class, ], "runner" => [ @@ -44,6 +45,9 @@ class Command extends AbstractCommand "server" => \Bow\Console\Command\ServerCommand::class, "worker" => \Bow\Console\Command\WorkerCommand::class, ], + "flush" => [ + "worker" => \Bow\Console\Command\WorkerCommand::class, + ], ]; /** @@ -79,5 +83,7 @@ public function call(string $command, string $action, ...$rest): mixed if (method_exists($instance, $method)) { return call_user_func_array([$instance, $method], $rest); } + + return null; } } diff --git a/src/Console/Command/GenerateKeyCommand.php b/src/Console/Command/GenerateKeyCommand.php index ec467429..30629a2e 100644 --- a/src/Console/Command/GenerateKeyCommand.php +++ b/src/Console/Command/GenerateKeyCommand.php @@ -25,7 +25,7 @@ public function generate(): void } $contents = file_get_contents($env_file); - $contents = preg_replace('@"APP_KEY"\s*:\s*".+?"@', '"APP_KEY": "' . $key . '"', $contents); + $contents = preg_replace('@"APP_KEY"\s*:\s*".*?"@', '"APP_KEY": "' . $key . '"', $contents); file_put_contents($env_file, $contents); diff --git a/src/Console/Command/GenerateQueueCommand.php b/src/Console/Command/GenerateQueueCommand.php new file mode 100644 index 00000000..b12241a3 --- /dev/null +++ b/src/Console/Command/GenerateQueueCommand.php @@ -0,0 +1,34 @@ +setting->getMigrationDirectory(), + $filename + ); + + $generator->write('model/queue', [ + 'className' => $filename + ]); + + echo Color::green('Queue migration created.'); + } +} diff --git a/src/Console/Command/MigrationCommand.php b/src/Console/Command/MigrationCommand.php index fbfbc0b6..4c99545f 100644 --- a/src/Console/Command/MigrationCommand.php +++ b/src/Console/Command/MigrationCommand.php @@ -274,9 +274,9 @@ private function createMigrationTable() 'create' ); - $generator->addColumn('migration', 'string', ['unique' => true]); - $generator->addColumn('batch', 'int'); - $generator->addColumn('created_at', 'datetime', [ + $generator->addString('migration', ['unique' => true]); + $generator->addInteger('batch'); + $generator->addDatetime('created_at', [ 'default' => 'CURRENT_TIMESTAMP', 'nullable' => true ]); diff --git a/src/Console/Command/WorkerCommand.php b/src/Console/Command/WorkerCommand.php index 4d4b4c1c..69c18305 100644 --- a/src/Console/Command/WorkerCommand.php +++ b/src/Console/Command/WorkerCommand.php @@ -16,8 +16,11 @@ class WorkerCommand extends AbstractCommand */ public function run(?string $connection = null): void { - $retry = (int) $this->arg->getParameter('--retry', 3); + $tries = (int) $this->arg->getParameter('--tries', 3); $default = $this->arg->getParameter('--queue', "default"); + $memory = (int) $this->arg->getParameter('--memory', 126); + $timout = (int) $this->arg->getParameter('--timout', 60); + $sleep = (int) $this->arg->getParameter('--sleep', 60); $queue = app("queue"); @@ -25,8 +28,38 @@ public function run(?string $connection = null): void $queue->setConnection($connection); } - $worker = new WorkerService(); + $worker = $this->getWorderService(); $worker->setConnection($queue->getAdapter()); - $worker->run($default, $retry); + $worker->run($default, $tries, $sleep, $timout, $memory); + } + + /** + * Flush the queue + * + * @param ?string $connection + * @return void + */ + public function flush(?string $connection = null) + { + $connection_queue = $this->arg->getParameter('--queue'); + + $queue = app("queue"); + + if (!is_null($connection)) { + $queue->setConnection($connection); + } + + $adapter = $queue->getAdapter(); + $adapter->flush($connection_queue); + } + + /** + * Get the worker service + * + * @return WorkerService + */ + private function getWorderService() + { + return new WorkerService(); } } diff --git a/src/Console/Console.php b/src/Console/Console.php index 2d2901e0..192caa12 100644 --- a/src/Console/Console.php +++ b/src/Console/Console.php @@ -7,8 +7,6 @@ use Bow\Configuration\Loader; use Bow\Console\Exception\ConsoleException; use Bow\Console\Traits\ConsoleTrait; -use Psy\Exception\FatalErrorException; -use RuntimeException; /** * @method static Console addCommand(string $command, callable $cb) @@ -79,7 +77,7 @@ class Console * @var array */ private const COMMAND = [ - 'add', 'migration', 'migrate', 'run', 'generate', 'gen', 'seed', 'help', 'launch', 'clear' + 'add', 'migration', 'migrate', 'run', 'generate', 'gen', 'seed', 'help', 'launch', 'clear', 'flush' ]; /** @@ -144,7 +142,7 @@ public function bind(Loader $kernel): void /** * Launch Bow task runner * - * @return void + * @return mixed * @throws */ public function run(): mixed @@ -406,7 +404,7 @@ private function generate(): void { $action = $this->arg->getAction(); - if (!in_array($action, ['key', 'resource', 'session', 'cache'])) { + if (!in_array($action, ['key', 'resource', 'session', 'cache', 'queue'])) { $this->throwFailsCommand('This action is not exists', 'help generate'); } @@ -436,6 +434,23 @@ private function clear(): void $this->command->call('clear', "make", $action); } + /** + * Flush the connections + * + * @return void + * @throws \ErrorException + */ + private function flush(): void + { + $action = $this->arg->getAction(); + + if (!in_array($action, ['worker'])) { + $this->throwFailsCommand('This action is not exists', 'help flush'); + } + + $this->command->call('flush', $action); + } + /** * Display global help or helper command. * @@ -458,9 +473,9 @@ private function help(?string $command = null): int \033[0;32mGENERATE\033[00m create a new app key and resources \033[0;33mgenerate:resource\033[00m Create new REST controller - \033[0;33mgenerate:session\033[00m For generate session table - \033[0;33mgenerate:cache\033[00m For generate cache table + \033[0;33mgenerate:table\033[00m For generate the preset table for session, cache, queue \033[0;33mgenerate:key\033[00m Create new app key + \033[0;33mflush:worker\033[00m Flush all queues \033[0;32mADD\033[00m Create a user class \033[0;33madd:middleware\033[00m Create new middleware @@ -545,7 +560,7 @@ private function help(?string $command = null): int --model=[model_name] Define the usable model \033[0;33m$\033[00m php \033[0;34mbow\033[00m generate:resource name [option] For create a new REST controller - \033[0;33m$\033[00m php \033[0;34mbow\033[00m generate:session For generate session table + \033[0;33m$\033[00m php \033[0;34mbow\033[00m generate:table For generate the table for session, cache, queue \033[0;33m$\033[00m php \033[0;34mbow\033[00m generate:key For generate a new APP KEY \033[0;33m$\033[00m php \033[0;34mbow\033[00m generate help For display this @@ -570,7 +585,7 @@ private function help(?string $command = null): int [option] run:server [--port=5000] [--host=localhost] [--php-settings="display_errors=on"] run:console [--include=filename.php] [--prompt=prompt_name] - run:worker [--queue=default] [--connexion=beanstalkd,sqs] [--retry-after=duration] + run:worker [--queue=default] [--connexion=beanstalkd,sqs,redis,database] [--tries=duration] [--sleep=duration] [--timeout=duration] \033[0;33m$\033[00m php \033[0;34mbow\033[00m run:console\033[00m Show psysh php REPL \033[0;33m$\033[00m php \033[0;34mbow\033[00m run:server\033[00m [option] Start local developpement server @@ -597,13 +612,23 @@ private function help(?string $command = null): int \033[0;33m$\033[00m php \033[0;34mbow\033[00m seed:all\033[00m Make seeding for all \033[0;33m$\033[00m php \033[0;34mbow\033[00m seed:table\033[00m table_name Make seeding for one table +U; + break; + + case 'flush': + echo <<throwFailsCommand("Please make php bow help for show whole docs !"); exit(1); - break; } exit(0); diff --git a/src/Console/stubs/event.stub b/src/Console/stubs/event.stub index 2b27e443..081e154e 100644 --- a/src/Console/stubs/event.stub +++ b/src/Console/stubs/event.stub @@ -4,11 +4,11 @@ namespace {baseNamespace}{namespace}; use Bow\Event\Contracts\AppEvent; use Bow\Event\Dispatchable; -use Bow\Queue\Traits\SerializesModels; +use Bow\Support\Serializes; class {className} implements AppEvent { - use Dispatchable, SerializesModels; + use Dispatchable, Serializes; /** * Create a new event instance. diff --git a/src/Console/stubs/middleware.stub b/src/Console/stubs/middleware.stub index 832fa3ac..aea551f8 100644 --- a/src/Console/stubs/middleware.stub +++ b/src/Console/stubs/middleware.stub @@ -15,7 +15,7 @@ class {className} implements BaseMiddleware * @param array $args * @return mixed */ - public function process(Request $request, callable $next, array $args = []): void + public function process(Request $request, callable $next, array $args = []): mixed { // Code Here diff --git a/src/Console/stubs/model/queue.stub b/src/Console/stubs/model/queue.stub new file mode 100644 index 00000000..02a9bbaf --- /dev/null +++ b/src/Console/stubs/model/queue.stub @@ -0,0 +1,38 @@ +create("queues", function (SQLGenerator $table) { + $table->addString('id', ["primary" => true]); + $table->addString('queue'); + $table->addText('payload'); + $table->addInteger('attempts', ["default" => 3]); + $table->addEnum('status', [ + "size" => ["waiting", "processing", "reserved", "failed", "done"], + "default" => "waiting", + ]); + $table->addDatetime('avalaibled_at'); + $table->addDatetime('reserved_at', ["nullable" => true, "default" => null]); + $table->addDatetime('created_at'); + }); + } + + /** + * Rollback migration + */ + public function rollback(): void + { + $this->dropIfExists("queues"); + if ($this->adapter->getName() === 'pgsql') { + $this->addSql("DROP TYPE IF EXISTS queue_status"); + } + } +} diff --git a/src/Container/Action.php b/src/Container/Action.php index 51386a0b..f2922ae8 100644 --- a/src/Container/Action.php +++ b/src/Container/Action.php @@ -12,7 +12,6 @@ use Bow\Support\Collection; use Bow\Database\Barry\Model; use InvalidArgumentException; -use Bow\Middleware\BaseMiddleware; use Bow\Contracts\ResponseInterface; use Bow\Router\Exception\RouterException; @@ -169,9 +168,13 @@ public function call(callable|string|array $actions, ?array $param = null): mixe * like [AppController::class, 'action'] */ if (count($actions) === 2) { - if (class_exists($actions[0])) { - $actions = [$actions[0] . '::' . $actions[1]]; + if (!class_exists($actions[0])) { + throw new InvalidArgumentException( + 'The controller ' . $actions[0] . ' is not exists', + E_USER_ERROR + ); } + $actions = [$actions[0] . '::' . $actions[1]]; } /** @@ -182,32 +185,6 @@ public function call(callable|string|array $actions, ?array $param = null): mixe $actions = (array) $actions['controller']; } - $functions = []; - - /** - * We normalize of the action to execute and - * creation of the dependency injection - */ - foreach ($actions as $key => $action) { - if (is_string($action)) { - array_push($functions, $this->controller($action)); - - continue; - } - - if (!is_callable($action)) { - continue; - } - - if (is_array($action) && $action[0] instanceof Closure) { - $injection = $this->injectorForClosure($action[0]); - } else { - $injection = $this->injectorForClosure($action); - } - - array_push($functions, ['action' => $action, 'injection' => $injection]); - } - /** * We load the middleware associated with the action */ @@ -215,14 +192,12 @@ public function call(callable|string|array $actions, ?array $param = null): mixe if (is_callable($middleware)) { if ($middleware instanceof Closure || is_array($middleware)) { $this->dispatcher->pipe($middleware); - continue; } } if (class_exists($middleware)) { $this->dispatcher->pipe($middleware); - continue; } @@ -237,12 +212,16 @@ public function call(callable|string|array $actions, ?array $param = null): mixe // We check if middleware if define via aliases if (!array_key_exists($middleware, $this->middlewares)) { - throw new RouterException(sprintf('%s is not define middleware.', $middleware), E_ERROR); + throw new RouterException( + sprintf('%s is not define middleware.', $middleware), + ); } // We check if the defined middleware is a valid middleware. if (!class_exists($this->middlewares[$middleware])) { - throw new RouterException(sprintf('%s is not a middleware class.', $middleware)); + throw new RouterException( + sprintf('%s is not a middleware class.', $middleware), + ); } // We add middleware into dispatch pipeline @@ -262,13 +241,37 @@ public function call(callable|string|array $actions, ?array $param = null): mixe case is_string($response): case is_array($response): case is_object($response): - case $response instanceof \Iterable: + case is_iterable($response): + case $response instanceof \Iterator: case $response instanceof ResponseInterface: return $response; case $response instanceof Model || $response instanceof Collection: return $response->toArray(); } + $functions = []; + + /** + * We normalize of the action to execute and + * creation of the dependency injection + */ + foreach ($actions as $key => $action) { + if (is_string($action)) { + array_push($functions, $this->controller($action)); + continue; + } + if (!is_callable($action)) { + continue; + } + if (is_array($action) && $action[0] instanceof Closure) { + $injection = $this->injectorForClosure($action[0]); + } else { + $injection = $this->injectorForClosure($action); + } + + array_push($functions, ['action' => $action, 'injection' => $injection]); + } + return $this->dispatchControllers($functions, $param); } @@ -311,19 +314,19 @@ private function dispatchControllers(array $functions, array $params): mixed /** * Successively launches a function list. * - * @param array|callable $function - * @param array $arg + * @param array|callable|string $function + * @param array $arguments * @return mixed * @throws ReflectionException */ - public function execute(array|callable $function, array $arg): mixed + public function execute(array|callable|string $function, array $arguments): mixed { if (is_callable($function)) { - return call_user_func_array($function, $arg); + return call_user_func_array($function, $arguments); } if (is_array($function)) { - return call_user_func_array($function, $arg); + return call_user_func_array($function, $arguments); } // We launch the controller loader if $cb is a String @@ -336,7 +339,7 @@ public function execute(array|callable $function, array $arg): mixed if (is_array($controller)) { return call_user_func_array( $controller['action'], - array_merge($controller['injection'], $arg) + array_merge($controller['injection'], $arguments) ); } @@ -431,17 +434,17 @@ public function injector(string $classname, ?string $method = null): array /** * Injection for closure * - * @param callable $closure + * @param Closure|callable $closure * @return array * @throws */ - public function injectorForClosure(callable $closure): array + public function injectorForClosure(Closure|callable $closure): array { $reflection = new ReflectionFunction($closure); - $parameters = $reflection->getParameters(); - - return $this->getInjectParameters($parameters); + return $this->getInjectParameters( + $reflection->getParameters() + ); } /** @@ -484,16 +487,16 @@ private function getInjectParameter($class): ?object { $class_name = $class->getName(); + if (in_array(strtolower($class_name), Action::INJECTION_EXCEPTION_TYPE)) { + return null; + } + if (!class_exists($class_name, true)) { throw new InvalidArgumentException( sprintf('class %s not exists', $class_name) ); } - if (in_array(strtolower($class_name), Action::INJECTION_EXCEPTION_TYPE)) { - return null; - } - if (method_exists($class_name, 'getInstance')) { return $class_name::getInstance(); } diff --git a/src/Container/Capsule.php b/src/Container/Capsule.php index cd832307..3c146174 100644 --- a/src/Container/Capsule.php +++ b/src/Container/Capsule.php @@ -145,11 +145,10 @@ public function bind(string $key, callable $value) * Register the instance of a class * * @param string $key - * @param Closure $value - * + * @param Closure|callable $value * @return void */ - public function factory($key, \Closure $value) + public function factory($key, Closure|callable $value) { $this->factories[$key] = $value; } @@ -157,15 +156,16 @@ public function factory($key, \Closure $value) /** * Saves the instance of a class * - * @param string $key + * @param string $key * @param mixed $instance - * * @return void */ - public function instance($key, $instance) + public function instance(string $key, mixed $instance): void { if (!is_object($instance)) { - throw new InvalidArgumentException('Parameter [2] is invalid'); + throw new InvalidArgumentException( + "The parameter $instance must be an object." + ); } $this->instances[$key] = $instance; @@ -178,7 +178,7 @@ public function instance($key, $instance) * @return mixed * @throws */ - private function resolve($key) + private function resolve($key): mixed { $reflection = new ReflectionClass($key); diff --git a/src/Database/Barry/Builder.php b/src/Database/Barry/Builder.php index e4fbd5a9..678210ed 100644 --- a/src/Database/Barry/Builder.php +++ b/src/Database/Barry/Builder.php @@ -53,13 +53,13 @@ public function get(array $columns = []): Model|Collection|null */ public function exists(?string $column = null, mixed $value = null): bool { - if (is_null($column) == null && is_null($value)) { + if (is_null($column) && is_null($value)) { return $this->count() > 0; } // If value is null and column is define // we make the column as value on model primary key name - if (!is_null($column) and is_null($value)) { + if (!is_null($column) && is_null($value)) { $value = $column; $column = (new $this->model())->getKey(); diff --git a/src/Database/Barry/Concerns/Relationship.php b/src/Database/Barry/Concerns/Relationship.php index 1b102538..046f8505 100644 --- a/src/Database/Barry/Concerns/Relationship.php +++ b/src/Database/Barry/Concerns/Relationship.php @@ -104,14 +104,14 @@ public function hasMany( * The has one relative * * @param string $related - * @param string $primary_key * @param string $foreign_key + * @param string $primary_key * @return HasOne */ public function hasOne( string $related, - ?string $primary_key = null, - ?string $foreign_key = null + ?string $foreign_key = null, + ?string $primary_key = null ): HasOne { $related_model = app()->make($related); @@ -124,6 +124,6 @@ public function hasOne( $foreign_key = rtrim($related_model->getTable(), 's') . '_id'; } - return new HasOne($related_model, $this, $primary_key, $foreign_key); + return new HasOne($related_model, $this, $foreign_key, $primary_key); } } diff --git a/src/Database/Barry/Model.php b/src/Database/Barry/Model.php index 5fcc7fb5..b23ca3f9 100644 --- a/src/Database/Barry/Model.php +++ b/src/Database/Barry/Model.php @@ -13,6 +13,7 @@ use Bow\Database\Exception\NotFoundException; use Bow\Database\Barry\Traits\ArrayAccessTrait; use Bow\Database\Barry\Traits\SerializableTrait; +use Bow\Database\Pagination; abstract class Model implements \ArrayAccess, \JsonSerializable { @@ -255,14 +256,23 @@ public static function findBy(string $column, mixed $value): Collection * @param mixed $id * @param array $select * - * @return Collection|static|null + * @return Collection|Model|null */ public static function findAndDelete( int | string | array $id, array $select = ['*'] - ): Model { + ): Collection|Model|null { $model = static::find($id, $select); + if (is_null($model)) { + return $model; + } + + if ($model instanceof Collection) { + $model->dropAll(); + return $model; + } + $model->delete(); return $model; @@ -306,8 +316,11 @@ public static function create(array $data): Model } // Check if the primary key existe on updating data - if (!array_key_exists($model->primary_key, $data)) { - if ($model->auto_increment && static::$builder->getAdapterName() !== "pgsql") { + if ( + !array_key_exists($model->primary_key, $data) + && static::$builder->getAdapterName() !== "pgsql" + ) { + if ($model->auto_increment) { $id_value = [$model->primary_key => null]; $data = array_merge($id_value, $data); } elseif ($model->primary_key_type == 'string') { @@ -319,11 +332,7 @@ public static function create(array $data): Model // Override the olds model attributes $model->setAttributes($data); - - if ($model->save() == 1) { - // Throw the onCreated event - $model->fireEvent('onCreated'); - } + $model->save(); return $model; } @@ -334,9 +343,9 @@ public static function create(array $data): Model * @param int $page_number * @param int $current * @param int $chunk - * @return array + * @return Pagination */ - public static function paginate(int $page_number, int $current = 0, ?int $chunk = null): array + public static function paginate(int $page_number, int $current = 0, ?int $chunk = null): Pagination { return static::query()->paginate($page_number, $current, $chunk); } @@ -349,7 +358,33 @@ public static function paginate(int $page_number, int $current = 0, ?int $chunk */ public static function deleted(callable $cb): void { - $env = static::formatEventName('onDeleted'); + $env = static::formatEventName('model.deleted'); + + event()->once($env, $cb); + } + + /** + * Allows to associate listener + * + * @param callable $cb + * @throws + */ + public static function deleting(callable $cb): void + { + $env = static::formatEventName('model.deleted'); + + event()->once($env, $cb); + } + + /** + * Allows to associate a listener + * + * @param callable $cb + * @throws + */ + public static function creating(callable $cb): void + { + $env = static::formatEventName('model.creating'); event()->once($env, $cb); } @@ -362,7 +397,20 @@ public static function deleted(callable $cb): void */ public static function created(callable $cb): void { - $env = static::formatEventName('onCreated'); + $env = static::formatEventName('model.created'); + + event()->once($env, $cb); + } + + /** + * Allows to associate a listener + * + * @param callable $cb + * @throws + */ + public static function updating(callable $cb): void + { + $env = static::formatEventName('model.updating'); event()->once($env, $cb); } @@ -375,7 +423,7 @@ public static function created(callable $cb): void */ public static function updated(callable $cb): void { - $env = static::formatEventName('onUpdated'); + $env = static::formatEventName('model.updated'); event()->once($env, $cb); } @@ -404,7 +452,7 @@ public static function query(): Builder if (!isset($properties['table']) || $properties['table'] == null) { $parts = explode('\\', static::class); - $table = Str::snake(end($parts)) . 's'; + $table = Str::lower(Str::snake(Str::plurial(end($parts)))); } else { $table = $properties['table']; } @@ -432,51 +480,71 @@ public static function query(): Builder } /** - * Save aliases on insert action + * Create the new row * + * @param Builder $model * @return int - * @throws */ - public function save() + private function writeRows(Builder $builder) { - $model = static::query(); + // Fire the creating event + $this->fireEvent('model.creating'); - /** - * Get the current primary key value - */ $primary_key_value = $this->getKeyValue(); - // If primary key value is null, we are going to start the creation of new row - if ($primary_key_value == null) { - // Insert information in the database - $row_affected = $model->insert($this->attributes); + // Insert information in the database + $row_affected = $builder->insert($this->attributes); - // We get a last insertion id value - if (static::$builder->getAdapterName() == 'pgsql') { + // We get a last insertion id value + if (static::$builder->getAdapterName() == 'pgsql') { + if ($this->auto_increment) { $sequence = $this->table . "_" . $this->primary_key . '_seq'; $primary_key_value = static::$builder->getPdo()->lastInsertId($sequence); - } else { - $primary_key_value = static::$builder->getPdo()->lastInsertId(); } + } else { + $primary_key_value = static::$builder->getPdo()->lastInsertId(); + } - $primary_key_value = !is_numeric($primary_key_value) ? $primary_key_value : (int) $primary_key_value; + if (is_null($primary_key_value) || $primary_key_value == 0) { + $primary_key_value = $this->attributes[$this->primary_key] ?? null; + } - // Set the primary key value - $this->attributes[$this->primary_key] = $primary_key_value; - $this->original = $this->attributes; + $primary_key_value = !is_numeric($primary_key_value) ? $primary_key_value : (int) $primary_key_value; - if ($row_affected == 1) { - $this->fireEvent('onCreated'); - } + // Set the primary key value + $this->attributes[$this->primary_key] = $primary_key_value; + $this->original = $this->attributes; - return $row_affected; + if ($row_affected == 1) { + $this->fireEvent('model.created'); + } + + return $row_affected; + } + + /** + * Save aliases on insert action + * + * @return int + * @throws + */ + public function save() + { + $builder = static::query(); + + // Get the current primary key value + $primary_key_value = $this->getKeyValue(); + + // If primary key value is null, we are going to start the creation of new row + if (is_null($primary_key_value)) { + return $this->writeRows($builder); } $primary_key_value = $this->transtypeKeyValue($primary_key_value); // Check the existent in database - if (!$model->exists($this->primary_key, $primary_key_value)) { - return 0; + if (!$builder->exists($this->primary_key, $primary_key_value)) { + return $this->writeRows($builder); } // We set the primary key value @@ -495,11 +563,15 @@ public function save() $update_data = $this->original; } - $updated = $model->where($this->primary_key, $primary_key_value)->update($update_data); + // Fire the updating event + $this->fireEvent('model.updating'); + + // Execute update model + $updated = $builder->where($this->primary_key, $primary_key_value)->update($update_data); // Fire the updated event if there are affected row if ($updated) { - $this->fireEvent('onUpdated'); + $this->fireEvent('model.updated'); } return $updated; @@ -515,31 +587,30 @@ private function transtypeKeyValue(mixed $primary_key_value): mixed { // Transtype value to the define primary key type if ($this->primary_key_type == 'int') { - $primary_key_value = (int) $primary_key_value; - } elseif ($this->primary_key_type == 'float') { - $primary_key_value = (float) $primary_key_value; - } elseif ($this->primary_key_type == 'double') { - $primary_key_value = (float) $primary_key_value; - } else { - $primary_key_value = (string) $primary_key_value; + return (int) $primary_key_value; } - return $primary_key_value; + if ($this->primary_key_type == 'float' || $this->primary_key_type == 'double') { + return (float) $primary_key_value; + } + + return (string) $primary_key_value; } /** * Delete a record * * @param array $attributes - * @return int + * @return int|bool * @throws */ - public function update(array $attributes): int + public function update(array $attributes): int|bool { $primary_key_value = $this->getKeyValue(); + // return 0 if the primary key is not define for update if ($primary_key_value == null) { - return 0; + return false; } $model = static::query(); @@ -558,9 +629,12 @@ public function update(array $attributes): int } } + // Fire the updating event + $this->fireEvent('model.updating'); + // When the data for updating list is empty, we load the original data if (count($data_for_updating) == 0) { - $this->fireEvent('onUpdated'); + $this->fireEvent('model.updated'); return true; } @@ -569,7 +643,7 @@ public function update(array $attributes): int // Fire the updated event if there are affected row if ($updated) { - $this->fireEvent('onUpdated'); + $this->fireEvent('model.updated'); } return $updated; @@ -595,12 +669,15 @@ public function delete(): int return 0; } + // Fire the deleting event + $this->fireEvent('model.deleting'); + // We apply the delete action $deleted = $model->where($this->primary_key, $primary_key_value)->delete(); // Fire the deleted event if there are affected row if ($deleted) { - $this->fireEvent('onDeleted'); + $this->fireEvent('model.deleted'); } return $deleted; @@ -629,7 +706,7 @@ public static function deleteBy($column, $value): int */ public function getKeyValue(): mixed { - return $this->original[$this->primary_key] ?? null; + return $this->original[$this->primary_key] ?? $this->attributes[$this->primary_key] ?? null; } /** @@ -642,6 +719,16 @@ public function getKey(): string return $this->primary_key; } + /** + * Retrieves the primary key key + * + * @return string + */ + public function getKeyType(): string + { + return $this->primary_key_type; + } + /** * Used to update the timestamp. * @@ -743,7 +830,15 @@ public function getTable(): string */ public function toArray(): array { - return $this->attributes; + $data = []; + + foreach ($this->attributes as $key => $value) { + if (!in_array($key, $this->hidden)) { + $data[$key] = $value; + } + } + + return $data; } /** @@ -791,7 +886,11 @@ public function __get(string $name): mixed $attribute_exists = isset($this->attributes[$name]); if (!$attribute_exists && method_exists($this, $name)) { - return $this->$name()->getResults(); + $result = $this->$name(); + if ($result instanceof Relation) { + return $result->getResults(); + } + return $result; } if (!$attribute_exists) { diff --git a/src/Database/Barry/Relation.php b/src/Database/Barry/Relation.php index 6caf3346..2a11d475 100644 --- a/src/Database/Barry/Relation.php +++ b/src/Database/Barry/Relation.php @@ -9,6 +9,20 @@ abstract class Relation { + /** + * The foreign key of the parent model. + * + * @var string + */ + protected string $foreign_key; + + /** + * The associated key on the parent model. + * + * @var string + */ + protected string $local_key; + /** * The parent model instance * @@ -54,6 +68,7 @@ public function __construct(Model $related, Model $parent) { $this->parent = $parent; $this->related = $related; + $this->query = $this->related::query(); // Build the constraint effect @@ -62,20 +77,6 @@ public function __construct(Model $related, Model $parent) } } - /** - * Set the base constraints on the relation query. - * - * @return void - */ - abstract public function addConstraints(): void; - - /** - * Get the results of the relationship. - * - * @return mixed - */ - abstract public function getResults(): mixed; - /** * Get the parent model. * @@ -108,9 +109,36 @@ public function __call(string $method, array $args) $result = call_user_func_array([$this->query, $method], (array) $args); if ($result === $this->query) { - return $this->query; + return $this; } return $result; } + + /** + * Create a new row of the related + * + * @param array $attributes + * @return Model + */ + public function create(array $attributes): Model + { + $attributes[$this->foreign_key] = $this->parent->getKeyValue(); + + return $this->related->create($attributes); + } + + /** + * Get the results of the relationship. + * + * @return mixed + */ + abstract public function getResults(): mixed; + + /** + * Set the base constraints on the relation query. + * + * @return void + */ + abstract public function addConstraints(): void; } diff --git a/src/Database/Barry/Relations/BelongsTo.php b/src/Database/Barry/Relations/BelongsTo.php index 68f92bfe..f31a483e 100644 --- a/src/Database/Barry/Relations/BelongsTo.php +++ b/src/Database/Barry/Relations/BelongsTo.php @@ -10,20 +10,6 @@ class BelongsTo extends Relation { - /** - * The foreign key of the parent model. - * - * @var string - */ - protected $foreign_key; - - /** - * The associated key on the parent model. - * - * @var string - */ - protected $local_key; - /** * Create a new belongs to relationship instance. * @@ -51,11 +37,12 @@ public function __construct( */ public function getResults(): ?Model { - $key = $this->query->getTable() . "_" . $this->local_key; - $cache = Cache::cache('file')->get($key); + $key = $this->query->getTable() . ":belongsto:" . $this->related->getTable() . ":" . $this->foreign_key; + + $cache = Cache::store('file')->get($key); if (!is_null($cache)) { - $related = new $this->related; + $related = new $this->related(); $related->setAttributes($cache); return $related; } @@ -63,7 +50,7 @@ public function getResults(): ?Model $result = $this->query->first(); if (!is_null($result)) { - Cache::cache('file')->add($key, $result->toArray(), 500); + Cache::store('file')->add($key, $result->toArray(), 500); } return $result; diff --git a/src/Database/Barry/Relations/BelongsToMany.php b/src/Database/Barry/Relations/BelongsToMany.php index 62406b8f..d123f41f 100644 --- a/src/Database/Barry/Relations/BelongsToMany.php +++ b/src/Database/Barry/Relations/BelongsToMany.php @@ -10,20 +10,6 @@ class BelongsToMany extends Relation { - /** - * The foreign key of the parent model. - * - * @var string - */ - protected $foreign_key; - - /** - * The associated key on the parent model. - * - * @var string - */ - protected $local_key; - /** * Create a new belongs to relationship instance. * @@ -47,7 +33,6 @@ public function __construct(Model $related, Model $parent, string $foreign_key, */ public function getResults(): Collection { - // TODO: Cache the result return $this->query->get(); } @@ -58,8 +43,14 @@ public function getResults(): Collection */ public function addConstraints(): void { - if (static::$has_constraints) { - // Todo + if (!static::$has_constraints) { + return; } + + // For belongs to relationships, which are essentially the inverse of has one + // or has many relationships, we need to actually query on the primary key + // of the related models matching on the foreign key that's on a parent. + $foreign_key_value = $this->parent->getAttribute($this->foreign_key); + $this->query->where($this->local_key, '=', $foreign_key_value); } } diff --git a/src/Database/Barry/Relations/HasMany.php b/src/Database/Barry/Relations/HasMany.php index 6cd9d6ec..eb38420d 100644 --- a/src/Database/Barry/Relations/HasMany.php +++ b/src/Database/Barry/Relations/HasMany.php @@ -10,20 +10,6 @@ class HasMany extends Relation { - /** - * The foreign key of the parent model. - * - * @var string - */ - protected string $foreign_key; - - /** - * The associated key on the parent model. - * - * @var string - */ - protected string $local_key; - /** * Create a new belongs to relationship instance. * @@ -50,7 +36,6 @@ public function __construct(Model $related, Model $parent, string $foreign_key, */ public function getResults(): Collection { - // TODO: Cache the result return $this->query->get(); } @@ -61,8 +46,6 @@ public function getResults(): Collection */ public function addConstraints(): void { - if (static::$has_constraints) { - // Todo - } + // } } diff --git a/src/Database/Barry/Relations/HasOne.php b/src/Database/Barry/Relations/HasOne.php index ec3f452f..880a784c 100644 --- a/src/Database/Barry/Relations/HasOne.php +++ b/src/Database/Barry/Relations/HasOne.php @@ -10,20 +10,6 @@ class HasOne extends Relation { - /** - * The foreign key of the parent model. - * - * @var string - */ - protected string $foreign_key; - - /** - * The associated key on the parent model. - * - * @var string - */ - protected string $local_key; - /** * Create a new belongs to relationship instance. * @@ -38,7 +24,6 @@ public function __construct(Model $related, Model $parent, string $foreign_key, $this->local_key = $local_key; $this->foreign_key = $foreign_key; - $this->query = $this->query->where($this->foreign_key, $this->parent->getKeyValue()); } /** @@ -48,11 +33,12 @@ public function __construct(Model $related, Model $parent, string $foreign_key, */ public function getResults(): ?Model { - $key = $this->query->getTable() . "_" . $this->local_key; - $cache = Cache::cache('file')->get($key); + $key = $this->query->getTable() . ":hasone:" . $this->related->getTable() . ":" . $this->foreign_key; + + $cache = Cache::store('file')->get($key); if (!is_null($cache)) { - $related = new $this->related; + $related = new $this->related(); $related->setAttributes($cache); return $related; } @@ -60,7 +46,7 @@ public function getResults(): ?Model $result = $this->query->first(); if (!is_null($result)) { - Cache::cache('file')->add($key, $result->toArray(), 500); + Cache::store('file')->add($key, $result->toArray(), 60); } return $result; @@ -73,8 +59,10 @@ public function getResults(): ?Model */ public function addConstraints(): void { - if (static::$has_constraints) { - // Todo + if (!static::$has_constraints) { + return; } + + $this->query = $this->query->where($this->foreign_key, $this->parent->getAttribute($this->local_key)); } } diff --git a/src/Database/Barry/Traits/CanSerialized.php b/src/Database/Barry/Traits/CanSerialized.php index 0734398e..49895f09 100644 --- a/src/Database/Barry/Traits/CanSerialized.php +++ b/src/Database/Barry/Traits/CanSerialized.php @@ -13,7 +13,7 @@ trait CanSerialized * * @return string */ - public function __sleep() + public function __sleep(): array { if ($this instanceof Model) { return ['attributes' => $this->attributes]; @@ -21,13 +21,4 @@ public function __sleep() return ['attributes' => $this->toArray()]; } - - /** - * __wakeup - * - * @return string - */ - public function __wakeup() - { - } } diff --git a/src/Database/Barry/Traits/EventTrait.php b/src/Database/Barry/Traits/EventTrait.php index 895fea23..9b535673 100644 --- a/src/Database/Barry/Traits/EventTrait.php +++ b/src/Database/Barry/Traits/EventTrait.php @@ -16,7 +16,9 @@ trait EventTrait */ private static function formatEventName(string $event): string { - return str_replace('\\', '', strtolower(Str::snake(static::class))) . '.' . Str::snake($event); + $class_name = str_replace('\\', '', strtolower(Str::snake(static::class))); + + return sprintf("%s.%s", $class_name, strtolower($event)); } /** @@ -26,7 +28,7 @@ private static function formatEventName(string $event): string */ private function fireEvent(string $event): void { - $env = $this->formatEventName($event); + $env = static::formatEventName($event); if (event()->bound($env)) { event()->emit($env, $this); diff --git a/src/Database/Collection.php b/src/Database/Collection.php index 078f0f26..0b2c6be4 100644 --- a/src/Database/Collection.php +++ b/src/Database/Collection.php @@ -4,9 +4,10 @@ namespace Bow\Database; +use Bow\Support\Collection as SupportCollection; use Bow\Database\Barry\Model; -class Collection extends \Bow\Support\Collection +class Collection extends SupportCollection { /** * @inheritdoc @@ -47,7 +48,13 @@ public function toArray(): array */ public function toJson(int $option = 0): string { - return json_encode($this->toArray(), $option = 0); + $data = []; + + foreach ($this->toArray() as $model) { + $data[] = $model->toArray(); + } + + return json_encode($data, $option = 0); } /** @@ -67,7 +74,7 @@ public function dropAll(): void */ public function __toString(): string { - return json_encode($this->toArray()); + return json_encode($this->all()); } /** @@ -75,6 +82,6 @@ public function __toString(): string */ public function jsonSerialize(): array { - return $this->toArray(); + return $this->all(); } } diff --git a/src/Database/Connection/Adapter/PostgreSQLAdapter.php b/src/Database/Connection/Adapter/PostgreSQLAdapter.php index 5ad0c8eb..6e92cfe7 100644 --- a/src/Database/Connection/Adapter/PostgreSQLAdapter.php +++ b/src/Database/Connection/Adapter/PostgreSQLAdapter.php @@ -60,6 +60,30 @@ public function connection(): void // Formatting connection parameters $dsn = sprintf("pgsql:host=%s;port=%s;dbname=%s", $hostname, $port, $this->config['database']); + if (isset($this->config['sslmode'])) { + $dsn .= ';sslmode=' . $this->config['sslmode']; + } + + if (isset($this->config['sslrootcert'])) { + $dsn .= ';sslrootcert=' . $this->config['sslrootcert']; + } + + if (isset($this->config['sslcert'])) { + $dsn .= ';sslcert=' . $this->config['sslcert']; + } + + if (isset($this->config['sslkey'])) { + $dsn .= ';sslkey=' . $this->config['sslkey']; + } + + if (isset($this->config['sslcrl'])) { + $dsn .= ';sslcrl=' . $this->config['sslcrl']; + } + + if (isset($this->config['application_name'])) { + $dsn .= ';application_name=' . $this->config['application_name']; + } + $username = $this->config["username"]; $password = $this->config["password"]; @@ -71,5 +95,9 @@ public function connection(): void // Build the PDO connection $this->pdo = new PDO($dsn, $username, $password, $options); + + if ($this->config["charset"]) { + $this->pdo->query('SET NAMES \'' . $this->config["charset"] . '\''); + } } } diff --git a/src/Database/Database.php b/src/Database/Database.php index 55a94dcc..1198b559 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -12,6 +12,7 @@ use Bow\Database\Connection\Adapter\MysqlAdapter; use Bow\Database\Connection\Adapter\SqliteAdapter; use Bow\Database\Connection\Adapter\PostgreSQLAdapter; +use ErrorException; class Database { @@ -76,9 +77,8 @@ public static function getInstance(): Database /** * Connection, starts the connection on the DB * - * @param null $name - * @return null|Database - * + * @param ?string $name + * @return ?Database * @throws ConnectionException */ public static function connection(?string $name = null): ?Database @@ -121,7 +121,7 @@ public static function connection(?string $name = null): ?Database } /** - * Get connexion name + * Get the connexion name * * @return string|null */ @@ -315,7 +315,7 @@ public static function delete(string $sql_statement, array $data = []): int } /** - * Load the query builder factory on table name + * Load the query builder factory on the table name * * @param string $table * @return QueryBuilder @@ -338,27 +338,17 @@ public static function table(string $table): QueryBuilder * @param callable $callback * @return void */ - public static function startTransaction(?callable $callback = null): void + public static function startTransaction(): void { static::verifyConnection(); if (!static::$adapter->getConnection()->inTransaction()) { static::$adapter->getConnection()->beginTransaction(); } - - if (is_callable($callback)) { - try { - call_user_func_array($callback, []); - - static::commit(); - } catch (DatabaseException $e) { - static::rollback(); - } - } } /** - * Check if database execution is in transaction + * Check if database execution is in the transaction * * @return bool */ @@ -389,6 +379,29 @@ public static function rollback(): void static::$adapter->getConnection()->rollBack(); } + /** + * Starting the start of a transaction wrapper on top of the callback + * + * @param callable $callback + * @return mixed + */ + public static function transaction(callable $callback): mixed + { + static::startTransaction(); + + try { + $result = call_user_func_array($callback, []); + + static::commit(); + + return $result; + } catch (DatabaseException $e) { + static::rollback(); + + throw $e; + } + } + /** * Starts the verification of the connection establishment * @@ -405,9 +418,9 @@ private static function verifyConnection(): void * Retrieves the identifier of the last record. * * @param ?string $name - * @return int|string + * @return int|string|PDO */ - public static function lastInsertId(?string $name = null): int|string + public static function lastInsertId(?string $name = null): int|string|PDO { static::verifyConnection(); @@ -475,6 +488,12 @@ public static function setPdo(PDO $pdo) */ public function __call(string $method, array $arguments) { + if (is_null(static::$instance)) { + throw new ErrorException( + "Unable to get database instance before configuration" + ); + } + if (method_exists(static::$instance, $method)) { return call_user_func_array( [static::$instance, $method], diff --git a/src/Database/Migration/Compose/MysqlCompose.php b/src/Database/Migration/Compose/MysqlCompose.php index 698e4d2c..3773bfe2 100644 --- a/src/Database/Migration/Compose/MysqlCompose.php +++ b/src/Database/Migration/Compose/MysqlCompose.php @@ -28,6 +28,7 @@ private function composeAddMysqlColumn(string $name, array $description): string // Transform attribute $default = $attribute['default'] ?? null; $size = $attribute['size'] ?? false; + $check = $attribute['check'] ?? false; $primary = $attribute['primary'] ?? false; $increment = $attribute['increment'] ?? false; $nullable = $attribute['nullable'] ?? false; @@ -35,6 +36,7 @@ private function composeAddMysqlColumn(string $name, array $description): string $unsigned = $attribute['unsigned'] ?? false; $after = $attribute['after'] ?? false; $first = $attribute['first'] ?? false; + $custom = $attribute['custom'] ?? false; // String to VARCHAR if ($raw_type == 'STRING') { @@ -45,15 +47,18 @@ private function composeAddMysqlColumn(string $name, array $description): string $size = 255; } - // Add column size - if ($size) { - if (in_array($raw_type, ['ENUM', 'CHECK'])) { - $size = (array) $size; - $size = "'" . implode("', '", $size) . "'"; - } + // Set the size + if (is_numeric($size)) { $type = sprintf('%s(%s)', $type, $size); } + // Add column size + if (in_array($raw_type, ['ENUM', 'CHECK'])) { + $check = (array) $size; + $check = "'" . implode("', '", $check) . "'"; + $type = sprintf('%s(%s)', $type, $check); + } + // Bind auto increment action if ($increment) { $type = sprintf('%s AUTO_INCREMENT', $type); @@ -100,6 +105,11 @@ private function composeAddMysqlColumn(string $name, array $description): string $type = sprintf('%s FIRST', $type); } + // Apply the custom definition + if ($custom) { + $type = sprintf('%s %s', $type, $custom); + } + return trim( sprintf('%s `%s` %s', $description['command'], $name, $type) ); diff --git a/src/Database/Migration/Compose/PgsqlCompose.php b/src/Database/Migration/Compose/PgsqlCompose.php index 7d2cf794..cad7418a 100644 --- a/src/Database/Migration/Compose/PgsqlCompose.php +++ b/src/Database/Migration/Compose/PgsqlCompose.php @@ -6,6 +6,23 @@ trait PgsqlCompose { + /** + * Define the query for create the custom type + * + * @var array + */ + protected array $custom_types = []; + + /** + * Get the custom type for pgsql + * + * @return array + */ + public function getCustomTypeQueries(): array + { + return $this->custom_types; + } + /** * Compose sql statement for pgsql * @@ -35,6 +52,7 @@ private function composeAddPgsqlColumn(string $name, array $description): string $unsigned = $attribute['unsigned'] ?? false; $after = $attribute['after'] ?? false; $first = $attribute['first'] ?? false; + $custom = $attribute['custom'] ?? false; if ($after || $first) { throw new SQLGeneratorException("The key first and after only work on mysql"); @@ -45,24 +63,28 @@ private function composeAddPgsqlColumn(string $name, array $description): string $type = 'VARCHAR'; } + // Redefine the size if (!$size && in_array($raw_type, ['VARCHAR', 'STRING', 'LONG VARCHAR'])) { $size = 255; } // Add column size - if ($size) { - if (in_array($raw_type, ['ENUM', 'CHECK'])) { - $size = (array) $size; - $size = "'" . implode("', '", $size) . "'"; - } - if (in_array($raw_type, ['ENUM', 'CHECK', 'VARCHAR', 'LONG VARCHAR', 'STRING'])) { - $type = sprintf('%s(%s)', $type, $size); - } + if (in_array($raw_type, ['VARCHAR', 'STRING', 'LONG VARCHAR']) && $size) { + $type = sprintf('%s(%s)', $type, $size); + } + + if (in_array($raw_type, ['ENUM', 'CHECK'])) { + $type = $this->formatCheckOrEnum($name, $raw_type, $attribute); + } + + // Bind precision + if ($raw_type == "DOUBLE") { + $type = sprintf('DOUBLE PRECISION', $type); } // Bind auto increment action if ($increment) { - $type = 'SERIAL'; + $type = in_array($raw_type, ["INT", "TINYINT", "SMALLINT"]) ? "SERIAL" : "BIGSERIAL"; } // Set column as primary key @@ -98,8 +120,13 @@ private function composeAddPgsqlColumn(string $name, array $description): string $type = sprintf('UNSIGNED %s', $type); } + // Apply the custom definition + if ($custom) { + $type = sprintf('%s %s', $type, $custom); + } + return trim( - sprintf('%s %s %s', $description['command'], $name, $type) + sprintf('%s "%s" %s', $description['command'], $name, $type) ); } @@ -117,4 +144,66 @@ private function dropColumnForPgsql(string $name): void $this->sqls[] = trim(sprintf('DROP COLUMN %s', $name)); } } + + /** + * Format the CHECK in ENUM + * + * @param string $name + * @param string $type + * @param array $attribute + * @return void + */ + private function formatCheckOrEnum($name, $type, $attribute): string + { + if ($type == "ENUM") { + $size = (array) $attribute['size']; + $size = "'" . implode("', '", $size) . "'"; + $table = preg_replace("/(ies)$/", "y", $this->table); + $table = preg_replace("/(s)$/", "", $table); + $this->custom_types[] = sprintf( + "CREATE TYPE %s_%s_enum AS ENUM(%s);", + $table, + $name, + $size + ); + + return sprintf('%s_%s_enum', $table, $name); + } + + if (count($attribute["check"]) === 3) { + [$column, $comparaison, $value] = $attribute["check"]; + if (is_array($value)) { + $value = "('" . implode("', '", $value) . "')"; + } + return sprintf('TEXT CHECK ("%s" %s %s)', $column, $comparaison, $value); + } + + [$column, $value] = $attribute["check"]; + + $comparaison = "="; + + if (is_string($value)) { + $value = "'" . addcslashes($value, "'") . "'"; + return sprintf('TEXT CHECK ("%s" %s %s)', $column, $comparaison, $value); + } + + $value = (array) $value; + + if (count($value) > 1) { + $comparaison = "IN"; + + foreach ($value as $key => $item) { + if (is_string($item)) { + $value[$key] = "'" . addcslashes($item, "'") . "'"; + } + } + + $value = "(" . implode(", ", $value) . ")"; + return sprintf('TEXT CHECK ("%s" %s %s)', $column, $comparaison, $value); + } + + $value = end($value); + + return sprintf('TEXT CHECK ("%s" %s %s)', $column, $comparaison, $value); + } } diff --git a/src/Database/Migration/Compose/SqliteCompose.php b/src/Database/Migration/Compose/SqliteCompose.php index 9be1e0e5..f23a86bd 100644 --- a/src/Database/Migration/Compose/SqliteCompose.php +++ b/src/Database/Migration/Compose/SqliteCompose.php @@ -33,6 +33,7 @@ private function composeAddSqliteColumn(string $name, array $description): strin $increment = $attribute['increment'] ?? false; $nullable = $attribute['nullable'] ?? false; $unique = $attribute['unique'] ?? false; + $custom = $attribute['custom'] ?? false; // String to VARCHAR if ($raw_type == 'STRING') { @@ -43,10 +44,6 @@ private function composeAddSqliteColumn(string $name, array $description): strin $size = 255; } - // Add column size - if ($size) { - } - // Set column as primary key if ($primary) { $type = sprintf('%s PRIMARY KEY', $type); @@ -79,6 +76,11 @@ private function composeAddSqliteColumn(string $name, array $description): strin $type = sprintf('%s DEFAULT %s', $type, $default); } + // Apply the custom definition + if ($custom) { + $type = sprintf('%s %s', $type, $custom); + } + return trim( sprintf('%s `%s` %s', $description['command'], $name, $type) ); diff --git a/src/Database/Migration/Migration.php b/src/Database/Migration/Migration.php index 4635f766..ecd9bc48 100644 --- a/src/Database/Migration/Migration.php +++ b/src/Database/Migration/Migration.php @@ -17,7 +17,7 @@ abstract class Migration * * @var AbstractConnection */ - private $adapter; + private AbstractConnection $adapter; /** * Migration constructor @@ -58,6 +58,16 @@ final public function connection(string $name): Migration return $this; } + /** + * Get adapter name + * + * @return string + */ + public function getAdapterName(): string + { + return $this->adapter->getName(); + } + /** * Drop table action * @@ -113,12 +123,21 @@ final public function create(string $table, callable $cb): Migration $engine = null; } - if ($this->adapter->getName() === 'pgsql') { - $sql = sprintf("CREATE TABLE %s (%s)%s;", $table, $generator->make(), $engine); - } else { + if ($this->adapter->getName() !== 'pgsql') { $sql = sprintf("CREATE TABLE `%s` (%s)%s;", $table, $generator->make(), $engine); + + return $this->executeSqlQuery($sql); + } + + foreach ($generator->getCustomTypeQueries() as $sql) { + try { + $this->executeSqlQuery($sql); + } catch (\Exception $exception) { + echo sprintf("%s\n", Color::yellow("Warning: " . $exception->getMessage())); + } } + $sql = sprintf("CREATE TABLE %s (%s)%s;", $table, $generator->make(), $engine); return $this->executeSqlQuery($sql); } @@ -210,11 +229,11 @@ private function executeSqlQuery(string $sql): Migration try { Database::statement($sql); } catch (\Exception $exception) { - echo sprintf("%s%s\n", Color::red("â–¶"), $sql); + echo sprintf("%s %s\n", Color::red("â–¶"), $sql); throw new MigrationException($exception->getMessage(), (int) $exception->getCode()); } - echo sprintf("%s%s\n", Color::green("â–¶"), $sql); + echo sprintf("%s %s\n", Color::green("â–¶"), $sql); return $this; } } diff --git a/src/Database/Migration/SQLGenerator.php b/src/Database/Migration/SQLGenerator.php index f819e3e5..9f82a22f 100644 --- a/src/Database/Migration/SQLGenerator.php +++ b/src/Database/Migration/SQLGenerator.php @@ -98,6 +98,19 @@ public function make(): string return $sql; } + /** + * Add a raw column definiton + * + * @param string $definition + * @return SQLGenerator + */ + public function addRaw(string $definition): SQLGenerator + { + $this->sqls[] = $definition; + + return $this; + } + /** * Add new column in the table * @@ -158,7 +171,7 @@ public function renameColumn(string $name, string $new): SQLGenerator } if ($this->adapter === 'pgsql') { - $this->sqls[] = sprintf("RENAME COLUMN %s TO %s", $name, $new); + $this->sqls[] = sprintf('RENAME COLUMN "%s" TO %s', $name, $new); } else { $this->sqls[] = sprintf("RENAME COLUMN `%s` TO `%s`", $name, $new); } diff --git a/src/Database/Migration/Shortcut/ConstraintColumn.php b/src/Database/Migration/Shortcut/ConstraintColumn.php index 00445b45..b4d10121 100644 --- a/src/Database/Migration/Shortcut/ConstraintColumn.php +++ b/src/Database/Migration/Shortcut/ConstraintColumn.php @@ -25,22 +25,35 @@ public function addForeign(string $name, array $attributes = []): SQLGenerator $on = ''; $references = ''; - $target = sprintf("%s_%s_foreign", $this->getTable(), $name); + + if ($this->adapter == "pgsql") { + $target = sprintf("\"%s_%s_foreign\"", $this->getTable(), $name); + } else { + $target = sprintf("%s_%s_foreign", $this->getTable(), $name); + } if (isset($attributes['on'])) { $on = strtoupper(' ON ' . $attributes['on']); } if (isset($attributes['references'], $attributes['table'])) { - $references = sprintf( - ' REFERENCES %s(%s)', - $attributes['table'], - $attributes['references'] - ); + if ($this->adapter === 'pgsql') { + $references = sprintf( + ' REFERENCES %s("%s")', + $attributes['table'], + $attributes['references'] + ); + } else { + $references = sprintf( + ' REFERENCES %s(`%s`)', + $attributes['table'], + $attributes['references'] + ); + } } if ($this->adapter === 'pgsql') { - $replacement = '%s %s FOREIGN KEY (%s)%s%s'; + $replacement = '%s %s FOREIGN KEY ("%s")%s%s'; } else { $replacement = '%s %s FOREIGN KEY (`%s`)%s%s'; } diff --git a/src/Database/Migration/Shortcut/MixedColumn.php b/src/Database/Migration/Shortcut/MixedColumn.php index c5884f66..e3b9de7b 100644 --- a/src/Database/Migration/Shortcut/MixedColumn.php +++ b/src/Database/Migration/Shortcut/MixedColumn.php @@ -49,10 +49,6 @@ public function addUuid(string $column, array $attribute = []): SQLGenerator return $this->addColumn($column, 'varchar', $attribute); } - if (!isset($attribute['default']) && $this->adapter === 'pgsql') { - $attribute['default'] = 'uuid_generate_v4()'; - } - return $this->addColumn($column, 'uuid', $attribute); } @@ -163,10 +159,14 @@ public function addEnum(string $column, array $attribute = []): SQLGenerator throw new SQLGeneratorException("The enum values should be define!"); } - if (is_array($attribute['size'])) { + if (!is_array($attribute['size'])) { throw new SQLGeneratorException("The enum values should be array"); } + if (count($attribute['size']) === 0) { + throw new SQLGeneratorException("The enum values cannot be empty."); + } + return $this->addColumn($column, 'enum', $attribute); } @@ -179,16 +179,20 @@ public function addEnum(string $column, array $attribute = []): SQLGenerator */ public function addCheck(string $column, array $attribute = []): SQLGenerator { - if (!isset($attribute['size'])) { + if (!isset($attribute['check'])) { throw new SQLGeneratorException("The check values should be define."); } - if (!is_array($attribute['size'])) { - throw new SQLGeneratorException("The enum values should be array."); + if (!is_array($attribute['check'])) { + throw new SQLGeneratorException("The check values should be array."); } - if (count($attribute['size']) === 0) { - throw new SQLGeneratorException("The enum values cannot be empty."); + if (count($attribute['check']) === 0) { + throw new SQLGeneratorException("The check values cannot be empty."); + } + + if (count($attribute['check']) === 0) { + throw new SQLGeneratorException("The check values cannot be empty."); } return $this->addColumn($column, 'check', $attribute); @@ -228,10 +232,6 @@ public function changeUuid(string $column, array $attribute = []): SQLGenerator return $this->changeColumn($column, 'varchar', $attribute); } - if (!isset($attribute['default']) && $this->adapter === 'pgsql') { - $attribute['default'] = 'uuid_generate_v4()'; - } - return $this->changeColumn($column, 'uuid', $attribute); } @@ -316,6 +316,18 @@ public function changeMacAddress(string $column, array $attribute = []): SQLGene */ public function changeEnum(string $column, array $attribute = []): SQLGenerator { + if (!isset($attribute['size'])) { + throw new SQLGeneratorException("The enum values should be define!"); + } + + if (!is_array($attribute['size'])) { + throw new SQLGeneratorException("The enum values should be array"); + } + + if (count($attribute['size']) === 0) { + throw new SQLGeneratorException("The enum values cannot be empty."); + } + return $this->changeColumn($column, 'enum', $attribute); } @@ -328,6 +340,22 @@ public function changeEnum(string $column, array $attribute = []): SQLGenerator */ public function changeCheck(string $column, array $attribute = []): SQLGenerator { + if (!isset($attribute['check'])) { + throw new SQLGeneratorException("The check values should be define."); + } + + if (!is_array($attribute['check'])) { + throw new SQLGeneratorException("The check values should be array."); + } + + if (count($attribute['check']) === 0) { + throw new SQLGeneratorException("The check values cannot be empty."); + } + + if (count($attribute['check']) === 0) { + throw new SQLGeneratorException("The check values cannot be empty."); + } + return $this->changeColumn($column, 'check', $attribute); } } diff --git a/src/Database/Pagination.php b/src/Database/Pagination.php new file mode 100644 index 00000000..2f271e43 --- /dev/null +++ b/src/Database/Pagination.php @@ -0,0 +1,59 @@ +next; + } + + public function hasNext(): bool + { + return $this->next != 0; + } + + public function perPage(): int + { + return $this->perPage; + } + + public function previous(): int + { + return $this->previous; + } + + public function hasPrevious(): bool + { + return $this->previous != 0; + } + + public function current(): int + { + return $this->current; + } + + public function items(): SupportCollection|DatabaseCollection + { + return $this->data; + } + + public function total(): int + { + return $this->total; + } +} diff --git a/src/Database/QueryBuilder.php b/src/Database/QueryBuilder.php index 05bea7b3..7d16ee86 100644 --- a/src/Database/QueryBuilder.php +++ b/src/Database/QueryBuilder.php @@ -6,7 +6,6 @@ use Bow\Database\Connection\AbstractConnection; use PDO; -use stdClass; use PDOStatement; use Bow\Support\Str; use Bow\Support\Util; @@ -88,9 +87,9 @@ class QueryBuilder implements \JsonSerializable /** * The PDO instance * - * @var \PDO + * @var PDO */ - protected ?\PDO $connection = null; + protected ?PDO $connection = null; /** * Define whether to retrieve information from the list @@ -279,6 +278,25 @@ public function whereRaw(string $where): QueryBuilder return $this; } + /** + * Add orWhere clause into the request + * + * WHERE column1 $comparator $value|column + * + * @param string $where + * @return QueryBuilder + */ + public function orWhereRaw(string $where): QueryBuilder + { + if ($this->where == null) { + $this->where = $where; + } else { + $this->where .= ' or ' . $where; + } + + return $this; + } + /** * orWhere, add a condition of type: * @@ -794,9 +812,9 @@ public function count($column = '*') * * @param $aggregate * @param string $column - * @return int|float + * @return mixed */ - private function aggregate($aggregate, $column): int|float + private function aggregate($aggregate, $column): mixed { $sql = 'select ' . $aggregate . '(' . $column . ') from ' . $this->table; @@ -860,9 +878,10 @@ public function get(array $columns = []): array|object|null $this->bind($statement, $this->where_data_binding); $this->where_data_binding = []; + $statement->execute(); - $data = Sanitize::make($statement->fetchAll()); + $data = $statement->fetchAll(); $statement->closeCursor(); @@ -1169,9 +1188,9 @@ public function drop(): bool * @param int $number_of_page * @param int $current * @param int $chunk - * @return array + * @return Pagination */ - public function paginate(int $number_of_page, int $current = 0, int $chunk = null): array + public function paginate(int $number_of_page, int $current = 0, ?int $chunk = null): Pagination { // We go to back page --$current; @@ -1192,6 +1211,10 @@ public function paginate(int $number_of_page, int $current = 0, int $chunk = nul $data = $this->jump($jump)->take($number_of_page)->get(); + if (is_array($data)) { + $data = collect($data); + } + // Reinitialisation of current query $this->where = $where; $this->join = $join; @@ -1202,18 +1225,18 @@ public function paginate(int $number_of_page, int $current = 0, int $chunk = nul // Grouped data if (is_int($chunk)) { - $data = array_chunk($data, $chunk); + $data = $data->chunk($chunk); } // Enables automatic paging. - return [ - 'next' => $current >= 1 && $rest_of_page > 0 ? $current + 1 : false, - 'previous' => ($current - 1) <= 0 ? 1 : ($current - 1), - 'total' => (int) ($rest_of_page + $current), - 'per_page' => $number_of_page, - 'current' => $current, - 'data' => $data - ]; + return new Pagination( + $current >= 1 && $rest_of_page > 0 ? $current + 1 : 0, + ($current - 1) <= 0 ? 1 : ($current - 1), + (int) ($rest_of_page + $current), + $number_of_page, + $current, + $data + ); } /** diff --git a/src/Database/README.md b/src/Database/README.md index a2aa586b..45b39e37 100644 --- a/src/Database/README.md +++ b/src/Database/README.md @@ -3,6 +3,7 @@ Bow Framework's database system is very simple database manager api with support: - MySQL +- PostGreSQL - SQLite Make database connexion is very simple diff --git a/src/Database/Redis.php b/src/Database/Redis.php new file mode 100644 index 00000000..3ce09a54 --- /dev/null +++ b/src/Database/Redis.php @@ -0,0 +1,168 @@ + 0) { + $options = compact('auth'); + } + + $options['backoff'] = [ + 'algorithm' => RedisClient::BACKOFF_ALGORITHM_DECORRELATED_JITTER, + 'base' => 500, + 'cap' => 750, + ]; + + static::$redis = new RedisClient(); + static::$redis->connect( + $config["host"], + $config["port"] ?? 6379, + $config["timeout"] ?? 2.5, + null, + 0, + 0, + $options + ); + + static::$redis->setOption(RedisClient::OPT_SERIALIZER, RedisClient::SERIALIZER_JSON); + + if (isset($config["prefix"])) { + static::$redis->setOption(RedisClient::OPT_PREFIX, $config["prefix"]); + } + + static::$redis->select($config["database"] ?? 0); + } + + /** + * Ping the redis service + * + * @param ?string $message + */ + public static function ping(?string $message = null) + { + static::$redis->ping($message); + } + + /** + * Set value on Redis + * + * @param string $key + * @param mixed $data + * @param integer|null $time + * @return boolean + */ + public static function set(string $key, mixed $data, ?int $time = null): bool + { + if (is_null(static::$instance)) { + static::$instance = static::getInstance(); + } + + $options = []; + + if (is_callable($data)) { + $content = $data(); + } else { + $content = $data; + } + + if (!is_null($time)) { + $options = [ + 'EX' => $time + ]; + } + + return static::$redis->set($key, $content, $options); + } + + /** + * Get the value from Redis + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public static function get(string $key, mixed $default = null): mixed + { + if (is_null(static::$instance)) { + static::$instance = static::getInstance(); + } + + if (!static::$redis->exists($key)) { + return is_callable($default) ? $default() : $default; + } + + $value = static::$redis->get($key); + + return is_null($value) ? $default : $value; + } + + /** + * Get the php-redis client + * + * @see https://github.com/phpredis/phpredis + * @return RedisClient + */ + public static function getClient(): RedisClient + { + if (is_null(static::$instance)) { + static::$instance = static::getInstance(); + } + + return static::$redis; + } +} diff --git a/src/Event/Contracts/EventListener.php b/src/Event/Contracts/EventListener.php index 2f5c2255..d46cff90 100644 --- a/src/Event/Contracts/EventListener.php +++ b/src/Event/Contracts/EventListener.php @@ -9,8 +9,8 @@ interface EventListener /** * Process the event * - * @param mixed $payload + * @param AppEvent $event * @return mixed */ - public function process($payload): void; + public function process(AppEvent $event): void; } diff --git a/src/Event/Contracts/EventShouldQueue.php b/src/Event/Contracts/EventShouldQueue.php new file mode 100644 index 00000000..8513b364 --- /dev/null +++ b/src/Event/Contracts/EventShouldQueue.php @@ -0,0 +1,7 @@ +event->process($this->payload); + } +} diff --git a/src/Event/Listener.php b/src/Event/Listener.php index 5eae44a8..d88e5c14 100644 --- a/src/Event/Listener.php +++ b/src/Event/Listener.php @@ -5,6 +5,7 @@ namespace Bow\Event; use Bow\Event\Contracts\EventListener; +use Bow\Event\Contracts\EventShouldQueue; class Listener { @@ -48,6 +49,10 @@ public function call(array $data = []): mixed if (is_string($callable) && class_exists($callable, true)) { $instance = app($callable); if ($instance instanceof EventListener) { + if ($instance instanceof EventShouldQueue) { + queue(new EventProducer($instance, $data)); + return null; + } $callable = [$instance, 'process']; } } diff --git a/src/Http/Client/HttpClient.php b/src/Http/Client/HttpClient.php index 5c74ab8a..3c65e117 100644 --- a/src/Http/Client/HttpClient.php +++ b/src/Http/Client/HttpClient.php @@ -13,7 +13,21 @@ class HttpClient * * @var array */ - private $attach = []; + private array $attach = []; + + /** + * Define the accept json header + * + * @var boolean + */ + private bool $accept_json = false; + + /** + * The headers collection + * + * @var array + */ + private $headers = []; /** * The curl instance @@ -41,7 +55,20 @@ public function __construct(?string $base_url = null) throw new \BadFunctionCallException('cURL php is require.'); } - $this->base_url = rtrim($base_url, "/"); + if (!is_null($base_url)) { + $this->base_url = rtrim($base_url, "/"); + } + } + + /** + * Set the base url + * + * @param string $url + * @return void + */ + public function setBaseUrl(string $url): void + { + $this->base_url = rtrim($url, "/"); } /** @@ -49,15 +76,22 @@ public function __construct(?string $base_url = null) * * @param string $url * @param array $data - * @return Parser + * @return Response */ - public function get(string $url, array $data = []): Parser + public function get(string $url, array $data = []): Response { - $this->initCurl($url); + if (count($data) > 0) { + $url = $url . "?" . http_build_query($data); + } - $this->addFields($data); + $this->init($url); + $this->applyCommonOptions(); + + curl_setopt($this->ch, CURLOPT_HTTPGET, true); - return new Parser($this->ch); + $content = $this->execute(); + + return new Response($this->ch, $content); } /** @@ -65,11 +99,11 @@ public function get(string $url, array $data = []): Parser * * @param string $url * @param array $data - * @return Parser + * @return Response */ - public function post(string $url, array $data = []): Parser + public function post(string $url, array $data = []): Response { - $this->initCurl($url); + $this->init($url); if (!empty($this->attach)) { curl_setopt($this->ch, CURLOPT_UPLOAD, true); @@ -82,8 +116,33 @@ public function post(string $url, array $data = []): Parser } $this->addFields($data); + $this->applyCommonOptions(); + + curl_setopt($this->ch, CURLOPT_POST, true); + + $content = $this->execute(); + + return new Response($this->ch, $content); + } + + /** + * Make put requete + * + * @param string $url + * @param array $data + * @return Response + */ + public function put(string $url, array $data = []): Response + { + $this->init($url); + $this->addFields($data); + $this->applyCommonOptions(); + + curl_setopt($this->ch, CURLOPT_PUT, true); - return new Parser($this->ch); + $content = $this->execute(); + + return new Response($this->ch, $content); } /** @@ -91,28 +150,32 @@ public function post(string $url, array $data = []): Parser * * @param string $url * @param array $data - * @return Parser + * @return Response */ - public function put(string $url, array $data = []): Parser + public function delete(string $url, array $data = []): Response { - $this->initCurl($url); + $this->init($url); + $this->addFields($data); + $this->applyCommonOptions(); - if (!curl_setopt($this->ch, CURLOPT_PUT, true)) { - $this->addFields($data); - } + curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, "DELETE"); - return new Parser($this->ch); + $content = $this->execute(); + + return new Response($this->ch, $content); } /** * Attach new file * * @param string $attach - * @return array + * @return HttpClient */ - public function addAttach(string|array $attach): array + public function addAttach(string|array $attach): HttpClient { - return $this->attach = (array) $attach; + $this->attach = (array) $attach; + + return $this; } /** @@ -123,39 +186,124 @@ public function addAttach(string|array $attach): array */ public function addHeaders(array $headers): HttpClient { - if (is_resource($this->ch)) { - $data = []; - - foreach ($headers as $key => $value) { - $data[] = $key . ': ' . $value; + foreach ($headers as $key => $value) { + if (!in_array(strtolower($key . ': ' . $value), array_map('strtolower', $this->headers))) { + $this->headers[] = $key . ': ' . $value; } - - curl_setopt($this->ch, CURLOPT_HTTPHEADER, $data); } return $this; } + /** + * Set the user agent + * + * @param string $user_agent + * @return HttpClient + */ + public function setUserAgent(string $user_agent): HttpClient + { + curl_setopt($this->ch, CURLOPT_USERAGENT, $user_agent); + + return $this; + } + + /** + * Set the json accept prop to format the sent content in json + * + * @return HttpClient + */ + public function acceptJson(): HttpClient + { + $this->accept_json = true; + + $this->addHeaders(["Content-Type" => "application/json"]); + + return $this; + } + /** * Reset alway connection * * @param string $url * @return void */ - private function initCurl(string $url): void + private function init(string $url): void { - $url = $this->base_url . "/" . trim($url, "/"); - $this->ch = curl_init($url); + if (!is_null($this->base_url)) { + $url = $this->base_url . "/" . trim($url, "/"); + } + + $this->ch = curl_init(trim($url, "/")); } /** - * Add field + * Add fields * * @param array $data * @return void */ private function addFields(array $data): void { - curl_setopt($this->ch, CURLOPT_POSTFIELDS, http_build_query($data)); + if (count($data) == 0) { + return; + } + + if ($this->accept_json) { + $payload = json_encode($data); + } else { + $payload = http_build_query($data); + } + + curl_setopt($this->ch, CURLOPT_POSTFIELDS, $payload); + } + + /** + * Close connection + * + * @return void + */ + private function close(): void + { + curl_close($this->ch); + } + + /** + * Execute request + * + * @return string + * @throws \Exception + */ + private function execute(): string + { + if ($this->headers) { + curl_setopt($this->ch, CURLOPT_HTTPHEADER, $this->headers); + } + + $content = curl_exec($this->ch); + $errno = curl_errno($this->ch); + + $this->close(); + + if ($content === false) { + throw new HttpClientException( + curl_strerror($errno), + $errno + ); + } + + return $content; + } + + /** + * Set Curl CURLOPT_RETURNTRANSFER option + * + * @return void + */ + private function applyCommonOptions() + { + curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($this->ch, CURLOPT_AUTOREFERER, true); } } diff --git a/src/Http/Client/HttpClientException.php b/src/Http/Client/HttpClientException.php new file mode 100644 index 00000000..9bd25482 --- /dev/null +++ b/src/Http/Client/HttpClientException.php @@ -0,0 +1,11 @@ +ch = $ch; - } - - /** - * Get raw content - * - * @return mixed - * @throws - */ - public function raw(): string - { - if (!$this->returnTransfertToRaw()) { - return null; - } - - return $this->execute(); - } - - /** - * Get response content - * - * @return mixed - * @throws - */ - public function getContent(): ?string - { - if (!$this->returnTransfertToPlain()) { - return null; - } - - return $this->execute(); - } - - /** - * Get response content as json - * - * @param array $default - * @return bool|string - * @throws - */ - public function toJson(?array $default = null): bool|string - { - if (!$this->returnTransfertToPlain()) { - if (is_array($default)) { - return json_encode($default); - } - - return false; - } - - $data = $this->raw(); - - return json_encode($data); - } - - /** - * Get response content as array - * - * @return mixed - * @throws - */ - public function toArray(): mixed - { - if (!$this->returnTransfert()) { - $this->close(); - - return ["error" => true, "message" => "Connat get information"]; - } - - return $this->execute(); - } - - /** - * Set Curl CURLOPT_RETURNTRANSFER option - * - * @return bool - */ - private function returnTransfert() - { - if (!curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true)) { - $this->close(); - - return false; - } - - return true; - } - - /** - * Set Curl CURLOPT_BINARYTRANSFER option - * - * @return bool - */ - private function returnTransfertToRaw() - { - if ($this->returnTransfert()) { - if (!curl_setopt($this->ch, CURLOPT_BINARYTRANSFER, true)) { - $this->close(); - - return false; - } - } - - return true; - } - - /** - * Set Curl CURLOPT_TRANSFERTEXT option - * - * @return bool - */ - private function returnTransfertToPlain() - { - if ($this->returnTransfert()) { - if (!curl_setopt($this->ch, CURLOPT_TRANSFERTEXT, true)) { - $this->close(); - - return false; - } - } - - return true; - } - - /** - * Execute request - * - * @return string - * @throws \Exception - */ - private function execute(): string - { - $data = curl_exec($this->ch); - - $this->error = curl_error($this->ch); - $this->errno = curl_errno($this->ch); - $this->header = curl_getinfo($this->ch); - $this->executed = true; - - $this->close(); - - if ($data === false) { - throw new \Exception(curl_strerror($this->errno)); - } - - return $data; - } - - /** - * Get the response headers - * - * @return array - * @throws - */ - public function getHeaders(): array - { - if (!$this->executed) { - $this->execute(); - } - - return $this->header; - } - - /** - * Get the response code - * - * @return ?int - * @throws - */ - public function getCode(): ?int - { - if (!$this->executed) { - $this->execute(); - } - - return $this->header['http_code'] ?? null; - } - - /** - * Get the response executing time - * - * @return ?int - * @throws - */ - public function getExecutionTime(): ?int - { - if (!$this->executed) { - $this->execute(); - } - - return $this->header['total_time'] ?? null; - } - - /** - * Get the request connexion time - * - * @return ?float - * @throws - */ - public function getConnexionTime(): ?float - { - if (!$this->executed) { - $this->execute(); - } - - return $this->header['connect_time'] ?? null; - } - - /** - * Get the response upload size - * - * @return ?float - * @throws - */ - public function getUploadSize(): ?float - { - if (!$this->executed) { - $this->execute(); - } - - return $this->header['size_upload'] ?? null; - } - - /** - * Get the request upload speed - * - * @return ?float - * @throws - */ - public function getUploadSpeed(): ?float - { - if (!$this->executed) { - $this->execute(); - } - - return $this->header['speed_upload'] ?? null; - } - - /** - * Get the download size - * - * @return ?float - * @throws - */ - public function getDownloadSize(): ?float - { - if (!$this->executed) { - $this->execute(); - } - - return $this->header['size_download'] ?? null; - } - - /** - * Get the downlad speed - * - * @return ?float - * @throws - */ - public function getDownloadSpeed(): ?float - { - if (!$this->executed) { - $this->execute(); - } - - return $this->header['speed_download'] ?? null; - } - - /** - * Get error message - * - * @return string - * @throws - */ - public function getErrorMessage(): string - { - if (!$this->executed) { - $this->execute(); - } - - return $this->error; - } - - /** - * Get error code - * - * @return int - * @throws - */ - public function getErrorNumber(): int - { - if (!$this->executed) { - $this->execute(); - } - - return $this->errno; - } - - /** - * Get the response content type - * - * @return ?string - * @throws - */ - public function getContentType(): ?string - { - if (!$this->executed) { - $this->execute(); - } - - return $this->header['content_type'] ?? null; - } - - /** - * Add attach file - * - * @param array $attach - * @return void - */ - public function addAttach($attach) - { - $this->attach = array_merge($this->attach, (array) $attach); - } - - /** - * Get attached files - * - * @return array - */ - public function getAttach(): array - { - return $this->attach; - } - - /** - * Set attach files - * - * @param array $attachs - * @return void - */ - public function setAttach(array $attachs): void - { - $this->attach = $attachs; - } - - /** - * Close connection - * - * @return void - */ - private function close(): void - { - curl_close($this->ch); - } -} diff --git a/src/Http/Client/Response.php b/src/Http/Client/Response.php new file mode 100644 index 00000000..2db90300 --- /dev/null +++ b/src/Http/Client/Response.php @@ -0,0 +1,224 @@ +error_message = curl_error($ch); + $this->errer_number = curl_errno($ch); + $this->headers = curl_getinfo($ch); + $this->content = $content; + } + + /** + * Get response content + * + * @return ?string + */ + public function getContent(): ?string + { + return $this->content; + } + + /** + * Get response content as json + * + * @return object|array + */ + public function toJson(?bool $associative = null): object|array + { + $content = $this->getContent(); + + return json_decode($content, $associative); + } + + /** + * Get response content as json + * + * @return array + */ + public function toArray(): array + { + return $this->toJson(true); + } + + /** + * Get the response headers + * + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * Get the response code + * + * @return ?int + */ + public function getCode(): ?int + { + return $this->headers['http_code'] ?? null; + } + + /** + * Alias of getCode + * + * @return ?int + */ + public function statusCode(): ?int + { + return $this->getCode(); + } + + /** + * Check if status code is successful + * + * @return bool + */ + public function isSuccessful(): bool + { + return $this->getCode() === 200 || $this->getCode() === 201; + } + + /** + * Check if status code is failed + * + * @return bool + */ + public function isFailed(): bool + { + return !$this->isSuccessful(); + } + + /** + * Get the response executing time + * + * @return ?int + */ + public function getExecutionTime(): ?int + { + return $this->headers['total_time'] ?? null; + } + + /** + * Get the request connexion time + * + * @return ?float + */ + public function getConnexionTime(): ?float + { + return $this->headers['connect_time'] ?? null; + } + + /** + * Get the response upload size + * + * @return ?float + */ + public function getUploadSize(): ?float + { + return $this->headers['size_upload'] ?? null; + } + + /** + * Get the request upload speed + * + * @return ?float + */ + public function getUploadSpeed(): ?float + { + return $this->headers['speed_upload'] ?? null; + } + + /** + * Get the download size + * + * @return ?float + */ + public function getDownloadSize(): ?float + { + return $this->headers['size_download'] ?? null; + } + + /** + * Get the downlad speed + * + * @return ?float + */ + public function getDownloadSpeed(): ?float + { + return $this->headers['speed_download'] ?? null; + } + + /** + * Get error message + * + * @return string + */ + public function getErrorMessage(): string + { + return $this->error_message ?? curl_strerror($this->errer_number); + } + + /** + * Get error code + * + * @return int + */ + public function getErrorNumber(): int + { + return $this->errer_number; + } + + /** + * Get the response content type + * + * @return ?string + */ + public function getContentType(): ?string + { + return $this->headers['content_type'] ?? null; + } +} diff --git a/src/Http/Exception/HttpException.php b/src/Http/Exception/HttpException.php index 87fda859..143c1b7a 100644 --- a/src/Http/Exception/HttpException.php +++ b/src/Http/Exception/HttpException.php @@ -15,11 +15,18 @@ class HttpException extends Exception */ protected $status = 'OK'; + /** + * Define the errors bags + * + * @var array + */ + protected array $error_bags = []; + /** * HttpException constructor * * @param string $message - * @param string $code + * @param int $code */ public function __construct(string $message, int $code = 200) { @@ -33,7 +40,7 @@ public function __construct(string $message, int $code = 200) * * @return string */ - public function getStatus() + public function getStatus(): string { return $this->status; } @@ -41,10 +48,30 @@ public function getStatus() /** * Get the status code * - * @return string + * @return int */ - public function getStatusCode() + public function getStatusCode(): int { return $this->getCode(); } + + /** + * Set the errors bags + * + * @param array $errors + */ + public function setErrorBags(array $errors) + { + $this->error_bags = $errors; + } + + /** + * Get the errors bags + * + * @return array + */ + public function getErrorBags(): array + { + return $this->error_bags; + } } diff --git a/src/Http/Exception/RequestException.php b/src/Http/Exception/RequestException.php index e351153b..14227228 100644 --- a/src/Http/Exception/RequestException.php +++ b/src/Http/Exception/RequestException.php @@ -4,8 +4,23 @@ namespace Bow\Http\Exception; -use ErrorException; - -class RequestException extends ErrorException +class RequestException extends HttpException { + /** + * Define the http response code + * + * @var int + */ + protected $code; + + /** + * Set the http code + * + * @param int $code + * @return void + */ + public function setCode(int $code) + { + $this->code = $code; + } } diff --git a/src/Http/Exception/ResponseException.php b/src/Http/Exception/ResponseException.php index 602f493d..d85ee80f 100644 --- a/src/Http/Exception/ResponseException.php +++ b/src/Http/Exception/ResponseException.php @@ -4,9 +4,7 @@ namespace Bow\Http\Exception; -use ErrorException; - -class ResponseException extends ErrorException +class ResponseException extends HttpException { /** * Define the http response code diff --git a/src/Http/Exception/UploadFileException.php b/src/Http/Exception/UploadedFileException.php similarity index 63% rename from src/Http/Exception/UploadFileException.php rename to src/Http/Exception/UploadedFileException.php index 3c27232d..23cc6494 100644 --- a/src/Http/Exception/UploadFileException.php +++ b/src/Http/Exception/UploadedFileException.php @@ -6,6 +6,6 @@ use ErrorException; -class UploadFileException extends ErrorException +class UploadedFileException extends ErrorException { } diff --git a/src/Http/HttpStatus.php b/src/Http/HttpStatus.php new file mode 100644 index 00000000..eb58e6ef --- /dev/null +++ b/src/Http/HttpStatus.php @@ -0,0 +1,149 @@ + 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 208 => 'Already Reported', + 226 => 'IM Used', + 300 => 'Multipe Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication', + 408 => 'Request Time Out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Payload Too Large', + 414 => 'URI Too Long', + 415 => 'Unsupport Media', + 416 => 'Range Not Statisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 421 => 'Misdirected Request', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 444 => 'Connection Closed Without Response', + 451 => 'Unavailable For Legal Reasons', + 499 => 'Client Closed Request', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + ]; + + public const CONTINUE = 100; + public const SWITCHING_PROTOCOLS = 101; + public const PROCESSING = 102; + public const OK = 200; + public const CREATED = 201; + public const ACCEPTED = 202; + public const NO_CONTENT = 204; + public const RESET_CONTENT = 205; + public const PARTIAL_CONTENT = 206; + public const MULTI_STATUS = 207; + public const ALREADY_REPORTED = 208; + public const IM_USED = 226; + public const MULTIPE_CHOICES = 300; + public const MOVED_PERMANENTLY = 301; + public const FOUND_ = 302; + public const SEE_OTHER = 303; + public const NOT_MODIFIED = 304; + public const USE_PROXY = 305; + public const TEMPORARY_REDIRECT = 307; + public const PERMANENT_REDIRECT = 308; + public const BAD_REQUEST = 400; + public const UNAUTHORIZED_ = 401; + public const PAYMENT_REQUIRED = 402; + public const FORBIDDEN_ = 403; + public const NOT_FOUND = 404; + public const METHOD_NOT_ALLOWED = 405; + public const NOT_ACCEPTABLE = 406; + public const PROXY_AUTHENTICATION = 407; + public const REQUEST_TIME_OUT = 408; + public const CONFLICT_ = 409; + public const GONE_ = 410; + public const LENGTH_REQUIRED = 411; + public const PRECONDITION_FAILED = 412; + public const PAYLOAD_TOO_LARGE = 413; + public const URI_TOO_LONG = 414; + public const UNSUPPORT_MEDIA = 415; + public const RANGE_NOT_STATISFIABLE = 416; + public const EXPECTATION_FAILED = 417; + public const I_M_A_TEAPOT = 418; + public const MISDIRECTED_REQUEST = 421; + public const UNPROCESSABLE_ENTITY = 422; + public const LOCKED_ = 423; + public const FAILED_DEPENDENCY = 424; + public const UPGRADE_REQUIRED = 426; + public const PRECONDITION_REQUIRED = 428; + public const TOO_MANY_REQUESTS = 429; + public const REQUEST_HEADER_FIELDS_TOO_LARGE = 431; + public const CONNECTION_CLOSED_WITHOUT_RESPONSE = 444; + public const UNAVAILABLE_FOR_LEGAL_REASONS = 451; + public const CLIENT_CLOSED_REQUEST = 499; + public const INTERNAL_SERVER_ERROR = 500; + public const NOT_IMPLEMENTED = 501; + public const BAD_GATEWAY = 502; + public const SERVICE_UNAVAILABLE = 503; + public const GATEWAY_TIMEOUT = 504; + public const HTTP_VERSION_NOT_SUPPORTED = 505; + + /** + * Get the message + * + * @param int $code + * @return string + */ + public static function getMessage(int $code): string + { + if (!isset(static::STATUS[$code])) { + throw new InvalidArgumentException("The code {$code} is not exists"); + } + + return static::STATUS[$code]; + } + + /** + * Get the codes + * + * @return array + */ + public static function getCodes(): array + { + return array_keys(static::STATUS); + } +} diff --git a/src/Http/Request.php b/src/Http/Request.php index c111548c..349d5fb6 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -4,12 +4,14 @@ namespace Bow\Http; -use Bow\Auth\Authentication; +use Bow\Support\Str; use Bow\Session\Session; +use Bow\Http\UploadedFile; use Bow\Support\Collection; -use Bow\Support\Str; +use Bow\Auth\Authentication; use Bow\Validation\Validate; use Bow\Validation\Validator; +use Bow\Http\Exception\BadRequestException; class Request { @@ -41,17 +43,35 @@ class Request */ private string $id; + /** + * Define the request captured + * + * @var bool + */ + private bool $capture = false; + /** * Request constructor * * @return mixed */ - private function __construct() + public function capture() { + if ($this->capture) { + return; + } + $data = []; + $this->id = "req_" . sha1(uniqid() . time()); if ($this->getHeader('content-type') == 'application/json') { - $data = json_decode(file_get_contents("php://input"), true); + try { + $data = json_decode(file_get_contents("php://input"), true, 1024, JSON_THROW_ON_ERROR); + } catch (\Throwable $e) { + throw new BadRequestException( + "The request json payload is invalid: " . $e->getMessage(), + ); + } $this->input = array_merge((array) $data, $_GET); } else { $data = $_POST ?? []; @@ -62,12 +82,14 @@ private function __construct() } foreach ($this->input as $key => $value) { - if (!is_array($value) && strlen($value) == 0) { + if (is_string($value) && strlen($value) == 0) { $value = null; } $this->input[$key] = $value; } + + $this->capture = true; } /** @@ -270,29 +292,29 @@ public function isDelete(): bool * Load the factory for FILES * * @param string $key - * @return UploadFile|Collection|null + * @return UploadedFile|Collection|null */ - public function file(string $key): UploadFile|Collection|null + public function file(string $key): UploadedFile|Collection|null { if (!isset($_FILES[$key])) { return null; } if (!is_array($_FILES[$key]['name'])) { - return new UploadFile($_FILES[$key]); + return new UploadedFile($_FILES[$key]); } $files = $_FILES[$key]; $collect = []; + foreach ($files['name'] as $key => $name) { - $file = [ + $collect[] = new UploadedFile([ 'name' => $name, 'type' => $files['type'][$key], 'size' => $files['size'][$key], 'error' => $files['error'][$key], 'tmp_name' => $files['tmp_name'][$key], - ]; - $collect[] = new UploadFile($file); + ]); } return new Collection($collect); @@ -340,6 +362,12 @@ public function isAjax(): bool return true; } + $content_type = $this->getHeader("content-type"); + + if ($content_type && str_contains($content_type, "application/json")) { + return true; + } + return false; } @@ -351,7 +379,7 @@ public function isAjax(): bool */ public function is($match): bool { - return (bool) preg_match('@' . $match . '@', $this->path()); + return (bool) preg_match('@' . addcslashes($match, "/*{()}[]$^") . '@', $this->path()); } /** @@ -362,7 +390,7 @@ public function is($match): bool */ public function isReferer($match): bool { - return (bool) preg_match('@' . $match . '@', $this->referer()); + return (bool) preg_match('@' . addcslashes($match, "/*{()}[]$^") . '@', $this->referer()); } /** @@ -486,7 +514,7 @@ public function getHeaders(): array * Get Request header * * @param string $key - * @return bool|string + * @return ?string */ public function getHeader($key): ?string { @@ -514,6 +542,16 @@ public function hasHeader($key): bool return isset($_SERVER[strtoupper($key)]); } + /** + * Get the client user agent + * + * @return ?string + */ + public function userAgent(): ?string + { + return $this->getHeader('USER_AGENT'); + } + /** * Get session information * @@ -574,7 +612,7 @@ public function only($exceptions) { $data = []; - if (! is_array($exceptions)) { + if (!is_array($exceptions)) { $exceptions = func_get_args(); } @@ -594,7 +632,7 @@ public function ignore($ignores) { $data = $this->input; - if (! is_array($ignores)) { + if (!is_array($ignores)) { $ignores = func_get_args(); } @@ -643,7 +681,7 @@ public function getBag(string $name) /** * Set the shared value in request bags * - * @param Array $bags + * @param array $bags * @return mixed */ public function setBags(array $bags) diff --git a/src/Http/Response.php b/src/Http/Response.php index 1c8019ac..6cadc32e 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -9,70 +9,6 @@ class Response implements ResponseInterface { - /** - * Valid http code list for the app Except that - * the user can himself redefine these codes - * if it uses the `header` function of php - */ - private static array $status_codes = [ - 100 => 'Continue', - 101 => 'Switching Protocols', - 102 => 'Processing', - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - 207 => 'Multi-Status', - 208 => 'Already Reported', - 226 => 'IM Used', - 300 => 'Multipe Choices', - 301 => 'Moved Permanently', - 302 => 'Found', - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 307 => 'Temporary Redirect', - 308 => 'Permanent Redirect', - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication', - 408 => 'Request Time Out', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Payload Too Large', - 414 => 'URI Too Long', - 415 => 'Unsupport Media', - 416 => 'Range Not Statisfiable', - 417 => 'Expectation Failed', - 418 => 'I\'m a teapot', - 421 => 'Misdirected Request', - 422 => 'Unprocessable Entity', - 423 => 'Locked', - 424 => 'Failed Dependency', - 426 => 'Upgrade Required', - 428 => 'Precondition Required', - 429 => 'Too Many Requests', - 431 => 'Request Header Fields Too Large', - 444 => 'Connection Closed Without Response', - 451 => 'Unavailable For Legal Reasons', - 499 => 'Client Closed Request', - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Timeout', - 505 => 'HTTP Version Not Supported', - ]; - /** * The Response instamce * @@ -237,9 +173,9 @@ public function addHeaders(array $headers): Response /** * Download the given file as an argument * - * @param string $file - * @param null $filename - * @param array $headers + * @param string $file + * @param ?string $filename + * @param array $headers * @return string */ public function download( @@ -276,15 +212,15 @@ public function download( /** * Modify http headers * - * @param int $code + * @param int $code * @return mixed */ public function status(int $code): Response { $this->code = $code; - if (in_array($code, array_keys(static::$status_codes), true)) { - @header('HTTP/1.1 ' . $code . ' ' . static::$status_codes[$code], $this->override, $code); + if (in_array($code, HttpStatus::getCodes(), true)) { + @header('HTTP/1.1 ' . $code . ' ' . HttpStatus::getMessage($code), $this->override, $code); } return $this; @@ -377,7 +313,7 @@ public function render(string $template, array $data = [], int $code = 200, arra $view = View::parse($template, $data); - $this->content = $view->sendContent(); + $this->content = $view->getContent(); return $this->buildHttpResponse(); } diff --git a/src/Http/ServerAccessControl.php b/src/Http/ServerAccessControl.php index 1ce4a4b5..8380488a 100644 --- a/src/Http/ServerAccessControl.php +++ b/src/Http/ServerAccessControl.php @@ -110,11 +110,11 @@ public function allowCredentials(): ServerAccessControl /** * Active Access-control-Max-Age * - * @param string $excepted + * @param float|int $excepted * @return ServerAccessControl * @throws ServerAccessControlException */ - public function maxAge(string $excepted): ServerAccessControl + public function maxAge(float|int $excepted): ServerAccessControl { if (!is_numeric($excepted)) { throw new ServerAccessControlException( @@ -123,7 +123,7 @@ public function maxAge(string $excepted): ServerAccessControl ); } - return $this->push('Access-Control-Max-Age', $excepted); + return $this->push('Access-Control-Max-Age', (string) $excepted); } /** diff --git a/src/Http/UploadFile.php b/src/Http/UploadedFile.php similarity index 96% rename from src/Http/UploadFile.php rename to src/Http/UploadedFile.php index f50d1241..e08986c1 100644 --- a/src/Http/UploadFile.php +++ b/src/Http/UploadedFile.php @@ -4,7 +4,7 @@ namespace Bow\Http; -class UploadFile +class UploadedFile { /** * @var array @@ -12,7 +12,7 @@ class UploadFile private array $file; /** - * UploadFile constructor. + * UploadedFile constructor. * * @param array $file */ @@ -24,9 +24,9 @@ public function __construct(array $file) /** * Get the file extension * - * @return string + * @return ?string */ - public function getExtension(): string + public function getExtension(): ?string { if (!isset($this->file['name'])) { return null; diff --git a/src/Mail/Driver/NativeDriver.php b/src/Mail/Driver/NativeDriver.php index 62b8cffb..db1a2d92 100644 --- a/src/Mail/Driver/NativeDriver.php +++ b/src/Mail/Driver/NativeDriver.php @@ -6,7 +6,6 @@ use Bow\Mail\Contracts\MailDriverInterface; use Bow\Mail\Message; -use Bow\Support\Str; use Bow\Mail\Exception\MailException; use InvalidArgumentException; diff --git a/src/Mail/Driver/SesDriver.php b/src/Mail/Driver/SesDriver.php index d528fe8f..e9380455 100644 --- a/src/Mail/Driver/SesDriver.php +++ b/src/Mail/Driver/SesDriver.php @@ -30,12 +30,25 @@ class SesDriver implements MailDriverInterface * @param array $config * @return void */ - public function __construct(array $config) + public function __construct(private array $config) { - $this->config_set = $config["config_set"] ?? false; - unset($config["config_set"]); + $this->config_set = $this->config["config_set"] ?? false; - $this->ses = new SesClient($config); + unset($this->config["config_set"]); + + $this->initializeSesClient(); + } + + /** + * Get the SES Instance + * + * @return SesClient + */ + public function initializeSesClient(): SesClient + { + $this->ses = new SesClient($this->config); + + return $this->ses; } /** diff --git a/src/Mail/Driver/SmtpDriver.php b/src/Mail/Driver/SmtpDriver.php index c6d5a154..01a90a76 100644 --- a/src/Mail/Driver/SmtpDriver.php +++ b/src/Mail/Driver/SmtpDriver.php @@ -108,12 +108,10 @@ public function send(Message $message): bool $error = true; // SMTP command - if ($this->username !== null) { + if ($message->getFrom() !== null) { + $this->write('MAIL FROM: <' . $message->getFrom() . '>', 250); + } elseif ($this->username !== null) { $this->write('MAIL FROM: <' . $this->username . '>', 250); - } else { - if ($message->getFrom() !== null) { - $this->write('MAIL FROM: <' . $message->getFrom() . '>', 250); - } } foreach ($message->getTo() as $value) { @@ -126,17 +124,22 @@ public function send(Message $message): bool $this->write('RCPT TO: ' . $to, 250); } + $message->setDefaultHeader(); + $this->write('DATA', 354); + $data = 'Subject: ' . $message->getSubject() . Message::END; $data .= $message->compileHeaders(); $data .= 'Content-Type: ' . $message->getType() . '; charset=' . $message->getCharset() . Message::END; $data .= 'Content-Transfer-Encoding: 8bit' . Message::END; $data .= Message::END . $message->getMessage() . Message::END; + $this->write($data); try { $this->write('.', 250); } catch (SmtpException $e) { + app("logger")->error($e->getMessage(), $e->getTraceAsString()); error_log($e->getMessage()); } @@ -167,7 +170,10 @@ private function connection() $sock = fsockopen($url, $this->port, $errno, $errstr, $this->timeout); if ($sock == null) { - throw new SocketException('Impossible to get connected to ' . $this->url . ':' . $this->port, E_USER_ERROR); + throw new SocketException( + 'Impossible to get connected to ' . $this->url . ':' . $this->port, + E_USER_ERROR + ); } $this->sock = $sock; @@ -194,7 +200,10 @@ private function connection() $secured = @stream_socket_enable_crypto($this->sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); if (!$secured) { - throw new ErrorException('Can not secure your connection with tls', E_ERROR); + throw new ErrorException( + 'Can not secure your connection with tls', + E_ERROR + ); } } @@ -225,9 +234,9 @@ private function disconnect() /** * Read the current connection stream. * - * @return string + * @return int */ - private function read() + private function read(): int { $s = null; @@ -254,7 +263,7 @@ private function read() * @param ?string $message * * @throws SmtpException - * @return string + * @return string|int|null */ private function write(string $command, ?int $code = null, ?string $message = null) { diff --git a/src/Mail/Mail.php b/src/Mail/Mail.php index 43734a40..187bf579 100644 --- a/src/Mail/Mail.php +++ b/src/Mail/Mail.php @@ -6,8 +6,16 @@ use Bow\Mail\Contracts\MailDriverInterface; use Bow\Mail\Exception\MailException; +use Bow\Mail\MailQueueProducer; use Bow\View\View; +/** + * @method mixed view(string $template, array $data, callable $cb) + * @method mixed queue(string $template, array $data, callable $cb) + * @method mixed queueOn(string $queue, string $template, array $data, callable $cb) + * @method mixed send($view, array|callable $data, ?callable $cb = null) + * @method mixed raw(string|array $to, string $subject, string $data, array $headers = []) + */ class Mail { /** @@ -59,8 +67,18 @@ public static function configure(array $config = []): MailDriverInterface static::$config = $config; } + if (!isset($config['driver'])) { + throw new MailException( + "The driver is not defined.", + E_USER_ERROR + ); + } + if (!in_array($config['driver'], array_keys(static::$drivers))) { - throw new MailException("The type is not known.", E_USER_ERROR); + throw new MailException( + "The driver is not defined.", + E_USER_ERROR + ); } $name = $config['driver']; @@ -76,8 +94,8 @@ public static function configure(array $config = []): MailDriverInterface /** * Push new driver * - * @param strinb $name - * @param strinb $class_name + * @param string $name + * @param string $class_name * @return bool */ public function pushDriver(string $name, string $class_name): bool @@ -104,18 +122,17 @@ public static function getInstance(): MailDriverInterface /** * @inheritdoc */ - public static function send($view, $bind, callable $cb) + public static function send(string $view, callable|array $data, ?callable $cb = null) { - if (is_callable($bind)) { - $cb = $bind; - $bind = []; + if (is_null($cb)) { + $cb = $data; + $data = []; } - $message = new Message(); - - $data = View::parse($view, $bind)->getContent(); + $content = View::parse($view, $data)->getContent(); - $message->setMessage($data); + $message = new Message(); + $message->setMessage($content); call_user_func_array($cb, [$message]); @@ -131,11 +148,9 @@ public static function send($view, $bind, callable $cb) * @param array $headers * @return mixed */ - public static function raw($to, $subject, $data, array $headers = []) + public static function raw(string|array $to, string $subject, string $data, array $headers = []) { - if (!is_array($to)) { - $to = [$to]; - } + $to = (array) $to; $message = new Message(); @@ -149,20 +164,111 @@ public static function raw($to, $subject, $data, array $headers = []) } /** - * Modify the smtp|mail driver + * Send message on queue + * + * @param string $template + * @param array $data + * @param callable $cb + * @return void + */ + public static function queue(string $template, array $data, callable $cb) + { + $message = new Message(); + + call_user_func_array($cb, [$message]); + + $producer = new MailQueueProducer($template, $data, $message); + + queue($producer); + } + + /** + * Send message on specific queue + * + * @param string $queue + * @param string $template + * @param array $data + * @param callable $cb + * @return void + */ + public static function queueOn(string $queue, string $template, array $data, callable $cb) + { + $message = new Message(); + + call_user_func_array($cb, [$message]); + + $producer = new MailQueueProducer($template, $data, $message); + + $producer->setQueue($queue); + + queue($producer); + } + + /** + * Send mail later + * + * @param integer $delay + * @param string $template + * @param array $data + * @param callable $cb + * @return void + */ + public static function later(int $delay, string $template, array $data, callable $cb) + { + $message = new Message(); + + call_user_func_array($cb, [$message]); + + $producer = new MailQueueProducer($template, $data, $message); + + $producer->setDelay($delay); + + queue($producer); + } + + /** + * Send mail later on specific queue + * + * @param integer $delay + * @param string $queue + * @param string $template + * @param array $data + * @param callable $cb + * @return void + */ + public static function laterOn(int $delay, string $queue, string $template, array $data, callable $cb) + { + $message = new Message(); + + call_user_func_array($cb, [$message]); + + $producer = new MailQueueProducer($template, $data, $message); + + $producer->setQueue($queue); + $producer->setDelay($delay); + + queue($producer); + } + + /** + * Modify the smtp|mail|ses driver * * @param string $driver - * @return SendInterface + * @return MailDriverInterface * @throws MailException */ - public static function setDriver($driver) + public static function setDriver(string $driver): MailDriverInterface { if (static::$config == null) { - throw new MailException('Mail non configurer.'); + throw new MailException( + 'Please configure the Mail service.' + ); } if (in_array($driver, array_keys(static::$drivers))) { - throw new MailException('The driver [$driver] is not available'); + throw new MailException( + "The driver $driver is not available" + ); } static::$config['driver'] = $driver; @@ -184,6 +290,9 @@ public function __call($name, $arguments) return call_user_func_array([static::class, $name], $arguments); } - throw new \ErrorException("This function does not exist. [$name]", E_ERROR); + throw new \ErrorException( + "This function $name does not existe", + E_ERROR + ); } } diff --git a/src/Mail/MailQueueProducer.php b/src/Mail/MailQueueProducer.php new file mode 100644 index 00000000..f4ad6af3 --- /dev/null +++ b/src/Mail/MailQueueProducer.php @@ -0,0 +1,65 @@ +bags = [ + "view" => $view, + "data" => $data, + "message" => $message, + ]; + } + + /** + * Process mail + * + * @return void + */ + public function process(): void + { + $message = $this->bags["message"]; + + $message->setMessage( + View::parse($this->bags["view"], $this->bags["data"])->getContent() + ); + + Mail::getInstance()->send($message); + } + + /** + * Send the processing exception + * + * @param Throwable $e + * @return void + */ + public function onException(Throwable $e) + { + $this->deleteJob(); + } +} diff --git a/src/Mail/Message.php b/src/Mail/Message.php index 39658375..6e067c46 100644 --- a/src/Mail/Message.php +++ b/src/Mail/Message.php @@ -47,14 +47,14 @@ class Message /** * Define the mail sender * - * @var string + * @var ?string */ private ?string $from = null; /** * The mail message * - * @var string + * @var ?string */ private ?string $message = null; @@ -145,12 +145,12 @@ public function to(string $to, ?string $name = null): Message /** * Define the receiver in list * - * @param array $sendTo + * @param array $recipients * @return $this */ - public function toList(array $sendTo): Message + public function toList(array $recipients): Message { - foreach ($sendTo as $name => $to) { + foreach ($recipients as $name => $to) { $this->to[] = $this->formatEmail($to, !is_int($name) ? $name : null); } @@ -397,6 +397,16 @@ public function setMessage(string $message, string $type = 'text/html') $this->message = $message; } + /** + * @see setMessage + * @param string $message + * @param string $type + */ + public function message(string $message, string $type = 'text/html') + { + $this->setMessage($message, $type); + } + /** * Get the headers * @@ -430,9 +440,9 @@ public function getSubject(): ?string /** * Get the sender * - * @return string + * @return ?string */ - public function getFrom(): string + public function getFrom(): ?string { return $this->from; } @@ -440,9 +450,9 @@ public function getFrom(): string /** * Get the email message * - * @return string + * @return ?string */ - public function getMessage(): string + public function getMessage(): ?string { return $this->message; } @@ -450,9 +460,9 @@ public function getMessage(): string /** * Get the email encoding * - * @return string + * @return ?string */ - public function getCharset(): string + public function getCharset(): ?string { return $this->charset; } @@ -460,11 +470,11 @@ public function getCharset(): string /** * Get Content-Type * - * @return string + * @return ?string */ - public function getType(): string + public function getType(): ?string { - return $this->type; + return is_null($this->type) ? 'text/html' : $this->type; } /** diff --git a/src/Middleware/AuthMiddleware.php b/src/Middleware/AuthMiddleware.php index 7c53ddd1..3a73bf79 100644 --- a/src/Middleware/AuthMiddleware.php +++ b/src/Middleware/AuthMiddleware.php @@ -15,7 +15,7 @@ class AuthMiddleware implements BaseMiddleware * Handle an incoming request. * * @param Request $request - * @param Callable $next + * @param callable $next * @param array $args * @return Redirect */ diff --git a/src/Middleware/CsrfMiddleware.php b/src/Middleware/CsrfMiddleware.php index 8eba888b..72eaf001 100644 --- a/src/Middleware/CsrfMiddleware.php +++ b/src/Middleware/CsrfMiddleware.php @@ -14,7 +14,7 @@ class CsrfMiddleware implements BaseMiddleware * Handle an incoming request. * * @param Request $request - * @param Callable $next + * @param callable $next * @param array $args * @throws */ @@ -33,14 +33,18 @@ public function process(Request $request, callable $next, array $args = []): mix response()->status(401); - throw new TokenMismatch('Token Mismatch'); + throw new TokenMismatch( + 'The request csrf token mismatch' + ); } if ($request->get('_token') == $request->session()->get('_token')) { return $next($request); } - throw new TokenMismatch('Token Mismatch'); + throw new TokenMismatch( + 'The request csrf token mismatch' + ); } /** diff --git a/src/Queue/Adapters/BeanstalkdAdapter.php b/src/Queue/Adapters/BeanstalkdAdapter.php index 31fdf17d..d11748ae 100644 --- a/src/Queue/Adapters/BeanstalkdAdapter.php +++ b/src/Queue/Adapters/BeanstalkdAdapter.php @@ -4,11 +4,11 @@ namespace Bow\Queue\Adapters; +use RuntimeException; use Pheanstalk\Pheanstalk; use Bow\Queue\ProducerService; use Bow\Queue\Adapters\QueueAdapter; -use Pheanstalk\Job as PheanstalkJob; -use RuntimeException; +use Pheanstalk\Contract\PheanstalkInterface; class BeanstalkdAdapter extends QueueAdapter { @@ -19,18 +19,6 @@ class BeanstalkdAdapter extends QueueAdapter */ private Pheanstalk $pheanstalk; - /** - * Determine the default watch name - * - * @var string - */ - private string $default = "default"; - - /** - * @var int - */ - private int $retry; - /** * Configure Beanstalkd driver * @@ -43,56 +31,17 @@ public function configure(array $queue): BeanstalkdAdapter throw new RuntimeException("Please install the pda/pheanstalk package"); } - $this->pheanstalk = Pheanstalk::create($queue["hostname"], $queue["port"], $queue["timeout"]); - - return $this; - } - - /** - * Get connexion - * - * @param string $name - * @return Pheanstalk - */ - public function setWatch(string $name): void - { - $this->default = $name; - } - - /** - * Get connexion - * - * @param int $retry - * @return Pheanstalk - */ - public function setRetry(int $retry): void - { - $this->retry = $retry; - } - - /** - * Delete a message from the Beanstalk queue. - * - * @param string $queue - * @param string|int $id - * @return void - */ - public function deleteJob(string $queue, string|int $id): void - { - $queue = $this->getQueue($queue); + $this->pheanstalk = Pheanstalk::create( + $queue["hostname"], + $queue["port"], + $queue["timeout"] + ); - $this->pheanstalk->useTube($queue)->delete(new PheanstalkJob($id, '')); - } + if (isset($queue["queue"])) { + $this->setQueue($queue["queue"]); + } - /** - * Get the queue or return the default. - * - * @param ?string $queue - * @return string - */ - public function getQueue(?string $queue = null): string - { - return $queue ?: $this->default; + return $this; } /** @@ -112,13 +61,25 @@ public function size(?string $queue = null): int * Queue a job * * @param ProducerService $producer - * @return QueueAdapter + * @return void */ public function push(ProducerService $producer): void { + $queues = (array) cache("beanstalkd:queues"); + + if (!in_array($producer->getQueue(), $queues)) { + $queues[] = $producer->getQueue(); + cache("beanstalkd:queues", $queues); + } + $this->pheanstalk ->useTube($producer->getQueue()) - ->put(serialize($producer), $producer->getDelay(), $producer->getRetry()); + ->put( + $this->serializeProducer($producer), + $this->getPriority($producer->getPriority()), + $producer->getDelay(), + $producer->getRetry() + ); } /** @@ -129,22 +90,91 @@ public function push(ProducerService $producer): void */ public function run(string $queue = null): void { - // we want jobs from 'testtube' only. + // we want jobs from define queue only. $queue = $this->getQueue($queue); $this->pheanstalk->watch($queue); // This hangs until a Job is produced. $job = $this->pheanstalk->reserve(); + if (is_null($job)) { + sleep($this->sleep ?? 5); + return; + } + try { $payload = $job->getData(); - $producer = unserialize($payload); + $producer = $this->unserializeProducer($payload); call_user_func([$producer, "process"]); + $this->sleep(2); $this->pheanstalk->touch($job); - $this->deleteJob($queue, $job->getId()); - } catch (\Exception $e) { - cache($job->getId(), $job->getData()); - $this->pheanstalk->release($job); + $this->sleep(2); + $this->pheanstalk->delete($job); + } catch (\Throwable $e) { + // Write the error log + error_log($e->getMessage()); + app('logger')->error($e->getMessage(), $e->getTrace()); + cache("job:failed:" . $job->getId(), $job->getData()); + + // Check if producer has been loaded + if (!isset($producer)) { + $this->pheanstalk->delete($job); + return; + } + + // Execute the onException method for notify the producer + // and let developper to decide if the job should be delete + $producer->onException($e); + + // Check if the job should be delete + if ($producer->jobShouldBeDelete()) { + $this->pheanstalk->delete($job); + } else { + $this->pheanstalk->release($job, $this->getPriority($producer->getPriority()), $producer->getDelay()); + } + $this->sleep(1); + } + } + + /** + * Flush the queue + * + * @return void + */ + public function flush(?string $queue = null): void + { + $queues = (array) $queue; + + if (count($queues) == 0) { + $queues = cache("beanstalkd:queues"); + } + + foreach ($queues as $queue) { + $this->pheanstalk->useTube($queue); + + while ($job = $this->pheanstalk->reserve()) { + $this->pheanstalk->delete($job); + } + } + } + + /** + * Get the priority + * + * @param int $priority + * @return int + */ + public function getPriority(int $priority): int + { + switch ($priority) { + case $priority > 2: + return 4294967295; + case 1: + return PheanstalkInterface::DEFAULT_PRIORITY; + case 0: + return 0; + default: + return PheanstalkInterface::DEFAULT_PRIORITY; } } } diff --git a/src/Queue/Adapters/DatabaseAdapter.php b/src/Queue/Adapters/DatabaseAdapter.php new file mode 100644 index 00000000..ddd9c27d --- /dev/null +++ b/src/Queue/Adapters/DatabaseAdapter.php @@ -0,0 +1,164 @@ +table = Database::table($queue["table"] ?? "queue_jobs"); + + return $this; + } + + /** + * Get the size of the queue. + * + * @param string $queue + * @return int + */ + public function size(?string $queue = null): int + { + return $this->table + ->where("queue", $this->getQueue($queue)) + ->count(); + } + + /** + * Queue a job + * + * @param ProducerService $producer + * @return QueueAdapter + */ + public function push(ProducerService $producer): void + { + $this->table->insert([ + "id" => $this->generateId(), + "queue" => $this->getQueue(), + "payload" => base64_encode($this->serializeProducer($producer)), + "attempts" => $this->tries, + "status" => "waiting", + "avalaibled_at" => date("Y-m-d H:i:s", time() + $producer->getDelay()), + "reserved_at" => null, + "created_at" => date("Y-m-d H:i:s"), + ]); + } + + /** + * Run the worker + * + * @param string|null $queue + * @return mixed + */ + public function run(string $queue = null): void + { + // we want jobs from define queue only. + $queue = $this->getQueue($queue); + $queues = $this->table + ->where("queue", $queue) + ->whereIn("status", ["waiting", "reserved"]) + ->get(); + + if (count($queues) == 0) { + $this->sleep($this->sleep ?? 5); + return; + } + + foreach ($queues as $job) { + try { + $producer = $this->unserializeProducer(base64_decode($job->payload)); + if (strtotime($job->avalaibled_at) >= time()) { + if (!is_null($job->reserved_at) && strtotime($job->reserved_at) < time()) { + continue; + } + $this->table->where("id", $job->id)->update([ + "status" => "processing", + ]); + $this->execute($producer, $job); + continue; + } + } catch (\Exception $e) { + // Write the error log + error_log($e->getMessage()); + app('logger')->error($e->getMessage(), $e->getTrace()); + cache("job:failed:" . $job->id, $job->payload); + + // Check if producer has been loaded + if (!isset($producer)) { + $this->sleep(1); + continue; + } + + // Execute the onException method for notify the producer + // and let developper to decide if the job should be delete + $producer->onException($e); + + // Check if the job should be delete + if ($producer->jobShouldBeDelete() || $job->attempts <= 0) { + $this->table->where("id", $job->id)->update([ + "status" => "failed", + ]); + $this->sleep(1); + continue; + } + + // Check if the job should be retry + $this->table->where("id", $job->id)->update([ + "status" => "reserved", + "attempts" => $job->attempts - 1, + "avalaibled_at" => date("Y-m-d H:i:s", time() + $producer->getDelay()), + "reserved_at" => date("Y-m-d H:i:s", time() + $producer->getRetry()) + ]); + + $this->sleep(1); + } + } + } + + /** + * Process the next job on the queue. + * + * @param ProducerService $producer + * @param mixed $job + */ + private function execute(ProducerService $producer, mixed $job) + { + call_user_func([$producer, "process"]); + $this->table->where("id", $job->id)->update([ + "status" => "done" + ]); + $this->sleep($this->sleep ?? 5); + } + + /** + * Flush the queue table + * + * @param ?string $queue + * @return void + */ + public function flush(?string $queue = null): void + { + if (is_null($queue)) { + $this->table->truncate(); + } else { + $this->table->where("queue", $queue)->delete(); + } + } +} diff --git a/src/Queue/Adapters/QueueAdapter.php b/src/Queue/Adapters/QueueAdapter.php index e2f4e341..714b19d1 100644 --- a/src/Queue/Adapters/QueueAdapter.php +++ b/src/Queue/Adapters/QueueAdapter.php @@ -8,14 +8,62 @@ abstract class QueueAdapter { + const EXIT_SUCCESS = 0; + const EXIT_ERROR = 1; + const EXIT_MEMORY_LIMIT = 12; + + /** + * Define the start time + * + * @var int + */ + protected float $start_time; + + /** + * Determine the default watch name + * + * @var string + */ + protected string $queue = "default"; + + /** + * The number of working attempts + * + * @var int + */ + protected int $tries = 3; + + /** + * Define the sleep time + * + * @var int + */ + protected int $sleep = 5; + + /** + * Make adapter configuration + * + * @param array $config + * @return QueueAdapter + */ + abstract public function configure(array $config): QueueAdapter; + + /** + * Push new producer + * + * @param ProducerService $producer + */ + abstract public function push(ProducerService $producer): void; + /** * Create producer serialization * * @param ProducerService $producer * @return string */ - public function serializeProducer(ProducerService $producer): string - { + public function serializeProducer( + ProducerService $producer + ): string { return serialize($producer); } @@ -25,56 +73,137 @@ public function serializeProducer(ProducerService $producer): string * @param string $producer * @return ProducerService */ - public function unserializeProducer(string $producer): ProducerService - { + public function unserializeProducer( + string $producer + ): ProducerService { return unserialize($producer); } /** - * Make adapter configuration + * Sleep the process * - * @param array $config - * @return QueueAdapter + * @param int $seconds + * @return void */ - abstract public function configure(array $config): QueueAdapter; + public function sleep(int $seconds): void + { + if ($seconds < 1) { + usleep($seconds * 1000000); + } else { + sleep($seconds); + } + } /** - * Watch the the queue name + * Laund the worker * - * @param string $queue + * @param integer $timeout + * @param integer $memory + * @return void */ - abstract public function setWatch(string $queue): void; + final public function work(int $timeout, int $memory): void + { + [$this->start_time, $jobs_processed] = [hrtime(true) / 1e9, 0]; + + if ($this->supportsAsyncSignals()) { + $this->listenForSignals(); + } + + while (true) { + $this->run(); + $jobs_processed++; + + if ($this->timeoutReached($timeout)) { + $this->kill(static::EXIT_ERROR); + } elseif ($this->memoryExceeded($memory)) { + $this->kill(static::EXIT_MEMORY_LIMIT); + } + } + } /** - * Set the retry value + * Kill the process. * - * @param int $retry + * @param int $status + * @return never */ - abstract public function setRetry(int $retry): void; + public function kill($status = 0) + { + if (extension_loaded('posix')) { + posix_kill(getmypid(), SIGKILL); + } + + exit($status); + } /** - * Push new producer + * Determine if the timeout is reached * - * @param ProducerService $producer + * @param int $timeout + * @return boolean */ - abstract public function push(ProducerService $producer): void; + protected function timeoutReached(int $timeout): bool + { + return (time() - $this->start_time) >= $timeout; + } /** - * Get the queue size + * Determine if the memory is exceeded * - * @param string $queue - * @return int + * @param int $memory_timit + * @return boolean */ - abstract public function size(string $queue): int; + private function memoryExceeded(int $memory_timit): bool + { + return (memory_get_usage() / 1024 / 1024) >= $memory_timit; + } + + /** + * Enable async signals for the process. + * + * @return void + */ + protected function listenForSignals() + { + pcntl_async_signals(true); + + pcntl_signal(SIGQUIT, fn () => error_log("bow worker exiting...")); + pcntl_signal(SIGTERM, fn () => error_log("bow worker exit...")); + pcntl_signal(SIGUSR2, fn () => error_log("bow worker restarting...")); + pcntl_signal(SIGCONT, fn () => error_log("bow worker continue...")); + } + + /** + * Determine if "async" signals are supported. + * + * @return bool + */ + protected function supportsAsyncSignals() + { + return extension_loaded('pcntl'); + } /** - * Delete a message from the queue. + * Set job tries * - * @param string $queue - * @param string|int $id + * @param int $tries * @return void */ - abstract public function deleteJob(string $queue, string|int $id): void; + public function setTries(int $tries): void + { + $this->tries = $tries; + } + + /** + * Set sleep time + * + * @param int $sleep + * @return void + */ + public function setSleep(int $sleep): void + { + $this->sleep = $sleep; + } /** * Get the queue or return the default. @@ -82,12 +211,60 @@ abstract public function deleteJob(string $queue, string|int $id): void; * @param ?string $queue * @return string */ - abstract public function getQueue(string $queue = null): string; + public function getQueue(?string $queue = null): string + { + return $queue ?: $this->queue; + } + + /** + * Generate the job id + * + * @return string + */ + public function generateId(): string + { + return sha1(uniqid(str_shuffle("abcdefghijklmnopqrstuvwxyz0123456789"), true)); + } + + /** + * Get the queue size + * + * @param string $queue + * @return int + */ + public function size(string $queue): int + { + return 0; + } /** * Start the worker server * * @param ?string $queue */ - abstract public function run(?string $queue = null): void; + public function run(?string $queue = null): void + { + // + } + + /** + * Flush the queue + * + * @param ?string $queue + * @return void + */ + public function flush(?string $queue = null): void + { + // + } + + /** + * Watch the queue name + * + * @param string $queue + */ + public function setQueue(string $queue): void + { + // + } } diff --git a/src/Queue/Adapters/SQSAdapter.php b/src/Queue/Adapters/SQSAdapter.php new file mode 100644 index 00000000..de98bbf9 --- /dev/null +++ b/src/Queue/Adapters/SQSAdapter.php @@ -0,0 +1,169 @@ +config = $config; + + $this->sqs = new SqsClient($config); + + return $this; + } + + /** + * Push a job onto the queue. + * + * @param ProducerService $producer + * @return void + */ + public function push(ProducerService $producer): void + { + $params = [ + 'DelaySeconds' => $producer->getDelay(), + 'MessageAttributes' => [ + "Title" => [ + 'DataType' => "String", + 'StringValue' => get_class($producer) + ], + "Id" => [ + "DataType" => "String", + "StringValue" => $this->generateId(), + ] + ], + 'MessageBody' => base64_encode($this->serializeProducer($producer)), + 'QueueUrl' => $this->config["url"] + ]; + + try { + $this->sqs->sendMessage($params); + } catch (AwsException $e) { + error_log($e->getMessage()); + } + } + + /** + * Get the size of the queue. + * + * @param string $queue + * @return int + */ + public function size(string $queue): int + { + $response = $this->sqs->getQueueAttributes([ + 'QueueUrl' => $this->getQueue($queue), + 'AttributeNames' => ['ApproximateNumberOfMessages'], + ]); + + $attributes = $response->get('Attributes'); + + return (int) $attributes['ApproximateNumberOfMessages']; + } + + /** + * Process the next job on the queue. + * + * @param ?string $queue + * @return void + */ + public function run(?string $queue = null): void + { + $this->sleep($this->sleep ?? 5); + + try { + $result = $this->sqs->receiveMessage([ + 'AttributeNames' => ['SentTimestamp'], + 'MaxNumberOfMessages' => 1, + 'MessageAttributeNames' => ['All'], + 'QueueUrl' => $this->config["url"], + 'WaitTimeSeconds' => 20, + ]); + $messages = $result->get('Messages'); + if (empty($messages)) { + $this->sleep(1); + return; + } + $message = $result->get('Messages')[0]; + $producer = $this->unserializeProducer(base64_decode($message["Body"])); + $delay = $producer->getDelay(); + call_user_func([$producer, "process"]); + $result = $this->sqs->deleteMessage([ + 'QueueUrl' => $this->config["url"], + 'ReceiptHandle' => $message['ReceiptHandle'] + ]); + } catch (AwsException $e) { + // Write the error log + error_log($e->getMessage()); + app('logger')->error($e->getMessage(), $e->getTrace()); + + if (isset($message)) { + cache( + "job:failed:" . $message["ReceiptHandle"], + $message["Body"] + ); + } + + // Check if producer has been loaded + if (!isset($producer)) { + $this->sleep(1); + return; + } + + // Execute the onException method for notify the producer + // and let developper to decide if the job should be delete + $producer->onException($e); + + // Check if the job should be delete + if ($producer->jobShouldBeDelete()) { + $result = $this->sqs->deleteMessage([ + 'QueueUrl' => $this->config["url"], + 'ReceiptHandle' => $message['ReceiptHandle'] + ]); + } else { + $result = $this->sqs->changeMessageVisibilityBatch([ + 'QueueUrl' => $this->config["url"], + 'Entries' => [ + 'Id' => $producer->getId(), + 'ReceiptHandle' => $message['ReceiptHandle'], + 'VisibilityTimeout' => $delay + ], + ]); + } + $this->sleep(1); + } + } +} diff --git a/src/Queue/Adapters/SyncAdapter.php b/src/Queue/Adapters/SyncAdapter.php new file mode 100644 index 00000000..50966f4f --- /dev/null +++ b/src/Queue/Adapters/SyncAdapter.php @@ -0,0 +1,42 @@ +config = $config; + + return $this; + } + + /** + * Queue a job + * + * @param ProducerService $producer + * @return void + */ + public function push(ProducerService $producer): void + { + $producer->process(); + } +} diff --git a/src/Queue/Connection.php b/src/Queue/Connection.php index d7255d76..270da540 100644 --- a/src/Queue/Connection.php +++ b/src/Queue/Connection.php @@ -6,6 +6,9 @@ use Bow\Queue\Adapters\QueueAdapter; use Bow\Queue\Adapters\BeanstalkdAdapter; +use Bow\Queue\Adapters\DatabaseAdapter; +use Bow\Queue\Adapters\SQSAdapter; +use Bow\Queue\Adapters\SyncAdapter; use ErrorException; class Connection @@ -31,6 +34,9 @@ class Connection */ private static array $connections = [ "beanstalkd" => BeanstalkdAdapter::class, + "sqs" => SQSAdapter::class, + "database" => DatabaseAdapter::class, + "sync" => SyncAdapter::class, ]; /** @@ -67,11 +73,13 @@ public static function pushConnection(string $name, string $classname): bool * Set connection * * @param string $connection - * @return void + * @return Connection */ - public function setConnection(string $connection): void + public function setConnection(string $connection): Connection { $this->connection = $connection; + + return $this; } /** @@ -84,6 +92,7 @@ public function getAdapter(): QueueAdapter $driver = $this->connection ?: $this->config["default"]; $connection = $this->config["connections"][$driver]; + $queue = new static::$connections[$driver](); return $queue->configure($connection); diff --git a/src/Queue/ProducerService.php b/src/Queue/ProducerService.php index f60ece1c..e3ef0cde 100644 --- a/src/Queue/ProducerService.php +++ b/src/Queue/ProducerService.php @@ -4,25 +4,25 @@ namespace Bow\Queue; -use Bow\Queue\Traits\SerializesModels; +use Bow\Support\Serializes; abstract class ProducerService { - use SerializesModels; + use Serializes; /** - * Define the delay + * Define the queue * - * @var int + * @var string */ - protected int $delay = 30; + protected string $queue = "default"; /** - * Define the queue + * Define the delay * - * @var string + * @var int */ - protected string $queue = "default"; + protected int $delay = 30; /** * Define the time of retry @@ -38,6 +38,37 @@ abstract class ProducerService */ protected int $priority = 1; + /** + * Determine if the job can be deleted + * + * @var bool + */ + protected bool $delete = false; + + /** + * Define the job id + * + * @return integer + */ + protected ?string $id = null; + + /** + * Define the job attempts + * + * @return integer + */ + protected int $attemps = 2; + + /** + * ProducerService constructor + * + * @return mixed + */ + public function __construct() + { + $this->id = sha1(uniqid(str_shuffle("abcdefghijklmnopqrstuvwxyz0123456789"), true)); + } + /** * Get the producer priority * @@ -48,6 +79,26 @@ final public function getPriority(): int return $this->priority; } + /** + * Get the producer id + * + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * Get the producer attemps + * + * @return int + */ + public function getAttemps(): int + { + return $this->attemps; + } + /** * Get the producer retry * @@ -78,6 +129,82 @@ final public function getDelay(): int return $this->delay; } + + + /** + * Set the producer attemps + * + * @param int $attemps + * @return void + */ + public function setAttemps(int $attemps) + { + $this->attemps = $attemps; + } + + /** + * Set the producer retry + * + * @param int $retry + * @return void + */ + final public function setRetry(int $retry) + { + $this->retry = $retry; + } + + /** + * Set the producer queue + * + * @param string $queue + * @return void + */ + final public function setQueue(string $queue) + { + $this->queue = $queue; + } + + /** + * Set the producer delay + * + * @param int $delay + */ + final public function setDelay(int $delay) + { + $this->delay = $delay; + } + + /** + * Delete the job from queue. + * + * @return void + */ + public function deleteJob(): void + { + $this->delete = true; + } + + /** + * Delete the job from queue. + * + * @return bool + */ + public function jobShouldBeDelete(): bool + { + return $this->delete; + } + + /** + * Get the job error + * + * @param \Throwable $e + * @return void + */ + public function onException(\Throwable $e) + { + // + } + /** * Process the producer * diff --git a/src/Queue/WorkerService.php b/src/Queue/WorkerService.php index 6d6f7429..df7c5f1f 100644 --- a/src/Queue/WorkerService.php +++ b/src/Queue/WorkerService.php @@ -30,16 +30,22 @@ public function setConnection(QueueAdapter $connection): void * Start the consumer * * @param string $queue - * @param integer $retry + * @param int $tries + * @param int $sleep + * @param int $timeout + * @param int $memory * @return void */ - public function run(string $queue = "default", int $retry = 60): void - { - $this->connection->setWatch($queue); - $this->connection->setRetry($retry); - - while (true) { - $this->connection->run(); - } + public function run( + string $queue = "default", + int $tries = 3, + int $sleep = 5, + int $timeout = 60, + int $memory = 128 + ): void { + $this->connection->setQueue($queue); + $this->connection->setTries($tries); + $this->connection->setSleep($sleep); + $this->connection->work($timeout, $memory); } } diff --git a/src/Router/Route.php b/src/Router/Route.php index 5313b119..850459c3 100644 --- a/src/Router/Route.php +++ b/src/Router/Route.php @@ -13,7 +13,7 @@ class Route /** * The callback has launched if the url of the query has matched. * - * @var callable + * @var mixed */ private mixed $cb; @@ -70,7 +70,7 @@ class Route * Route constructor * * @param string $path - * @param callable $cb + * @param mixed $cb * * @throws */ @@ -124,11 +124,7 @@ public function middleware(array|string $middleware): Route return $this; } - if (!isset($this->cb['middleware'])) { - $this->cb['middleware'] = $middleware; - } else { - $this->cb['middleware'] = array_merge((array) $this->cb['middleware'], $middleware); - } + $this->cb['middleware'] = !isset($this->cb['middleware']) ? $middleware : array_merge((array) $this->cb['middleware'], $middleware); return $this; } @@ -142,11 +138,7 @@ public function middleware(array|string $middleware): Route */ public function where(array|string $where, $regex_constraint = null): Route { - if (is_array($where)) { - $other_rule = $where; - } else { - $other_rule = [$where => $regex_constraint]; - } + $other_rule = is_array($where) ? $where : [$where => $regex_constraint]; $this->with = array_merge($this->with, $other_rule); diff --git a/src/Router/Router.php b/src/Router/Router.php index 44208640..2cd16ffb 100644 --- a/src/Router/Router.php +++ b/src/Router/Router.php @@ -113,6 +113,7 @@ public function setBaseRoute(string $base_route): void * Note: Disable only you run on test env * * @param bool $auto_csrf + * @return void */ public function setAutoCsrf(bool $auto_csrf): void { @@ -151,21 +152,17 @@ public function prefix(string $prefix, callable $cb): Router /** * Allows to associate a global middleware on an route * - * @param array $middlewares + * @param array|string $middlewares * @return Router */ - public function middleware(array $middlewares): Router + public function middleware(array|string $middlewares): Router { $middlewares = (array) $middlewares; $collection = []; foreach ($middlewares as $middleware) { - if (class_exists($middleware, true)) { - $collection[] = [new $middleware(), 'process']; - } else { - $collection[] = $middleware; - } + $collection[] = class_exists($middleware, true) ? [new $middleware(), 'process'] : $middleware; } return new Router($this->method, $this->magic_method, $this->base_route, $collection); @@ -223,16 +220,14 @@ public function route(array $definition): void * * @param string $path * @param callable|string|array $cb - * @return Router + * @return Route * @throws */ - public function any(string $path, callable|string|array $cb): Router + public function any(string $path, callable|string|array $cb): Route { - foreach (['options', 'patch', 'post', 'delete', 'put', 'get'] as $method) { - $this->$method($path, $cb); - } + $methods = array_map('strtoupper', ['options', 'patch', 'post', 'delete', 'put', 'get']); - return $this; + return $this->pushHttpVerb($methods, $path, $cb); } /** @@ -338,65 +333,72 @@ public function code(int $code, callable|array|string $cb): Router * @param array $methods * @param string $path * @param callable|string|array $cb - * @return Router + * @return Route */ - public function match(array $methods, string $path, callable|string|array $cb): Router + public function match(array $methods, string $path, callable|string|array $cb): Route { - foreach ($methods as $method) { - if ($this->method == strtoupper($method)) { - $this->pushHttpVerb(strtoupper($method), $path, $cb); - } - } + $methods = array_map('strtoupper', $methods); - return $this; + return $this->pushHttpVerb($methods, $path, $cb); } /** * Add other HTTP verbs [PUT, DELETE, UPDATE, HEAD, PATCH] * - * @param string $method + * @param string|array $methods * @param string $path * @param callable|array|string $cb * @return Route */ - private function pushHttpVerb(string $method, string $path, callable|string|array $cb): Route + private function pushHttpVerb(string|array $methods, string $path, callable|string|array $cb): Route { - if ($this->magic_method) { + $methods = (array) $methods; + + if (!$this->magic_method) { + return $this->routeLoader($methods, $path, $cb); + } + + foreach ($methods as $key => $method) { if ($this->magic_method === $method) { - $method = $this->magic_method; + $methods[$key] = $this->magic_method; } } - return $this->routeLoader($method, $path, $cb); + return $this->routeLoader($methods, $path, $cb); } /** * Start loading a route. * - * @param string $method + * @param string|array $method * @param string $path - * @param Callable|string|array $cb + * @param callable|string|array $cb * @return Route */ - private function routeLoader(string $method, string $path, callable|string|array $cb): Route + private function routeLoader(string|array $methods, string $path, callable|string|array $cb): Route { + $methods = (array) $methods; + $path = '/' . trim($path, '/'); // We build the original path based on the Router loader $path = $this->base_route . $this->prefix . $path; - // We define the current route and current method - $this->current = ['path' => $path, 'method' => $method]; - // We add the new route $route = new Route($path, $cb); $route->middleware($this->middlewares); - static::$routes[$method][] = $route; + foreach ($methods as $method) { + static::$routes[$method][] = $route; + + // We define the current route and current method + $this->current = ['path' => $path, 'method' => $method]; - if (app_env('APP_ENV') != 'production' && $this->auto_csrf === true) { - if (in_array($method, ['POST', 'DELETE', 'PUT'])) { + if ( + $this->auto_csrf === true + && in_array($method, ['POST', 'DELETE', 'PUT']) + ) { $route->middleware('csrf'); } } diff --git a/src/Session/Cookie.php b/src/Session/Cookie.php index 3b183957..ece2490c 100644 --- a/src/Session/Cookie.php +++ b/src/Session/Cookie.php @@ -75,7 +75,7 @@ public static function get(string $key, mixed $default = null): mixed public static function all(): array { foreach ($_COOKIE as $key => $value) { - $_COOKIE[$key] = Crypto::decrypt($value); + $_COOKIE[$key] = json_decode(Crypto::decrypt($value)); } return $_COOKIE; @@ -87,10 +87,6 @@ public static function all(): array * @param string|int $key * @param mixed $data * @param int $expirate - * @param string $path - * @param string $domain - * @param bool $secure - * @param bool $http * * @return bool */ @@ -98,21 +94,17 @@ public static function set( $key, $data, $expirate = 3600, - $path = null, - $domain = null, - $secure = false, - $http = true ) { - $data = Crypto::encrypt($data); + $data = Crypto::encrypt(json_encode($data)); return setcookie( $key, $data, time() + $expirate, - $path, - $domain, - $secure, - $http + config('session.path'), + config('session.domain'), + config('session.secure'), + config('session.httponly') ); } @@ -132,10 +124,12 @@ public static function remove(string $key): mixed if (!static::$is_decrypt[$key]) { $old = Crypto::decrypt($_COOKIE[$key]); + unset(static::$is_decrypt[$key]); } - static::set($key, null, -1000); + static::set($key, '', -1000); + unset($_COOKIE[$key]); return $old; diff --git a/src/Session/Driver/ArrayDriver.php b/src/Session/Driver/ArrayDriver.php index 6639434f..d8d4b7ad 100644 --- a/src/Session/Driver/ArrayDriver.php +++ b/src/Session/Driver/ArrayDriver.php @@ -40,9 +40,9 @@ public function destroy(string $session_id): bool * Garbage collector * * @param int $max_lifetime - * @return bool|void + * @return int|false */ - public function gc(int $max_lifetime): bool + public function gc(int $max_lifetime): int|false { foreach ($this->sessions as $session_id => $content) { if ($this->sessions[$session_id]['time'] <= $this->createTimestamp()) { @@ -50,7 +50,7 @@ public function gc(int $max_lifetime): bool } } - return true; + return 1; } /** diff --git a/src/Session/Driver/DatabaseDriver.php b/src/Session/Driver/DatabaseDriver.php index 9324ecb4..e032f51e 100644 --- a/src/Session/Driver/DatabaseDriver.php +++ b/src/Session/Driver/DatabaseDriver.php @@ -4,7 +4,6 @@ namespace Bow\Session\Driver; -use Bow\Support\Capsule; use Bow\Database\QueryBuilder; use Bow\Database\Database as DB; @@ -74,15 +73,15 @@ public function destroy(string $session_id): bool * Garbage collector for cleans old sessions * * @param int $max_lifetime - * @return bool + * @return int|false */ - public function gc(int $max_lifetime): bool + public function gc(int $max_lifetime): int|false { $this->sessions() ->where('time', '<', $this->createTimestamp()) ->delete(); - return true; + return 1; } /** diff --git a/src/Session/Driver/FilesystemDriver.php b/src/Session/Driver/FilesystemDriver.php index 50e240f4..4adc9403 100644 --- a/src/Session/Driver/FilesystemDriver.php +++ b/src/Session/Driver/FilesystemDriver.php @@ -54,9 +54,9 @@ public function destroy(string $session_id): bool * Garbage collector * * @param int $maxlifetime - * @return bool + * @return int|false */ - public function gc(int $maxlifetime): int + public function gc(int $maxlifetime): int|false { foreach (glob($this->save_path . "/*") as $file) { if (filemtime($file) + $maxlifetime < $this->createTimestamp() && file_exists($file)) { @@ -64,7 +64,7 @@ public function gc(int $maxlifetime): int } } - return true; + return 1; } /** diff --git a/src/Session/Session.php b/src/Session/Session.php index 205c1336..f4950062 100644 --- a/src/Session/Session.php +++ b/src/Session/Session.php @@ -159,7 +159,9 @@ private function initializeDriver(): void $driver = $this->driver[$this->config['driver']] ?? null; if (is_null($driver)) { - throw new SessionException('The driver ' . $this->config['driver'] . ' is not valid'); + throw new SessionException( + 'The driver ' . $this->config['driver'] . ' is not valid' + ); } switch ($this->config['driver']) { @@ -174,13 +176,16 @@ private function initializeDriver(): void $handler = new $driver(); break; default: - throw new SessionException('Cannot set the session driver'); - break; + throw new SessionException( + 'Cannot set the session driver' + ); } // Set the session driver if (!@session_set_save_handler($handler, true)) { - throw new SessionException('Cannot set the session driver'); + throw new SessionException( + 'Cannot set the session driver' + ); } } diff --git a/src/Session/SessionConfiguration.php b/src/Session/SessionConfiguration.php index 6bcdf88f..607c11aa 100644 --- a/src/Session/SessionConfiguration.php +++ b/src/Session/SessionConfiguration.php @@ -16,9 +16,9 @@ class SessionConfiguration extends Configuration public function create(Loader $config): void { $this->container->bind('session', function () use ($config) { - $session = Session::configure($config['session']); + $session = Session::configure((array) $config['session']); - Tokenize::makeCsrfToken($config['session.lifetime']); + Tokenize::makeCsrfToken((int) $config['session.lifetime']); // Reboot the old request values Session::getInstance()->add('__bow.old', []); diff --git a/src/Storage/Contracts/FilesystemInterface.php b/src/Storage/Contracts/FilesystemInterface.php index 8c47906d..c891a67d 100644 --- a/src/Storage/Contracts/FilesystemInterface.php +++ b/src/Storage/Contracts/FilesystemInterface.php @@ -4,7 +4,7 @@ namespace Bow\Storage\Contracts; -use Bow\Http\UploadFile; +use Bow\Http\UploadedFile; use InvalidArgumentException; interface FilesystemInterface @@ -12,13 +12,13 @@ interface FilesystemInterface /** * Store directly the upload file * - * @param UploadFile $file + * @param UploadedFile $file * @param string $location * @param array $option - * @return bool + * @return array|bool|string * @throws InvalidArgumentException */ - public function store(UploadFile $file, ?string $location = null, array $option = []): array|bool; + public function store(UploadedFile $file, ?string $location = null, array $option = []): array|bool|string; /** * Write following a file specify diff --git a/src/Storage/Exception/ServiceNotFoundException.php b/src/Storage/Exception/ServiceNotFoundException.php index dfedb2f0..f8ecc7b9 100644 --- a/src/Storage/Exception/ServiceNotFoundException.php +++ b/src/Storage/Exception/ServiceNotFoundException.php @@ -19,18 +19,19 @@ class ServiceNotFoundException extends ErrorException * Set the service name * * @param string $service_name - * - * @return void + * @return ServiceNotFoundException */ - public function setService($service_name) + public function setService(string $service_name): ServiceNotFoundException { $this->service_name = $service_name; + + return $this; } /** * Get the service name * - * @return void + * @return string */ public function getService() { diff --git a/src/Storage/Service/DiskFilesystemService.php b/src/Storage/Service/DiskFilesystemService.php index 4320fcc1..46f00782 100644 --- a/src/Storage/Service/DiskFilesystemService.php +++ b/src/Storage/Service/DiskFilesystemService.php @@ -4,7 +4,7 @@ namespace Bow\Storage\Service; -use Bow\Http\UploadFile; +use Bow\Http\UploadedFile; use Bow\Storage\Contracts\FilesystemInterface; use InvalidArgumentException; @@ -51,14 +51,14 @@ public function getBaseDirectory(): string /** * Function to upload a file * - * @param UploadFile $file + * @param UploadedFile $file * @param string|array $location * @param array $option * - * @return bool + * @return array|bool|string * @throws InvalidArgumentException */ - public function store(UploadFile $file, string|array $location = null, array $option = []): bool + public function store(UploadedFile $file, string|array $location = null, array $option = []): array|bool|string { if (is_array($location)) { $option = $location; @@ -237,7 +237,7 @@ public function copy(string $target, string $source): bool } if (!$this->exists($source)) { - $this->makeDirectory(dirname($source), true); + $this->makeDirectory(dirname($source)); } return (bool) file_put_contents($source, $this->get($target)); diff --git a/src/Storage/Service/FTPService.php b/src/Storage/Service/FTPService.php index 523fc1f0..9eef6d2b 100644 --- a/src/Storage/Service/FTPService.php +++ b/src/Storage/Service/FTPService.php @@ -4,7 +4,7 @@ namespace Bow\Storage\Service; -use Bow\Http\UploadFile; +use Bow\Http\UploadedFile; use Bow\Storage\Contracts\ServiceInterface; use Bow\Storage\Exception\ResourceException; use InvalidArgumentException; @@ -111,8 +111,8 @@ public function connect() $this->connection = $connection; $this->login(); - $this->setConnectionRoot(); - $this->setConnectionPassiveMode(); + $this->changePath(); + $this->activePassiveMode(); } /** @@ -139,15 +139,8 @@ private function login(): bool { ['username' => $username, 'password' => $password] = $this->config; - // We disable error handling to avoid credentials leak :+1: - set_error_handler( - fn () => error_log("set_error_handler muted for hidden the ftp credential to user") - ); - $is_logged_in = ftp_login($this->connection, $username, $password); - restore_error_handler(); - if ($is_logged_in) { return true; } @@ -165,12 +158,12 @@ private function login(): bool } /** - * Set the connection root. + * Change path. * * @param string $path * @return void */ - public function setConnectionRoot(string $path = '') + public function changePath(?string $path = null) { $base_path = $path ?: $this->config['root']; @@ -178,7 +171,6 @@ public function setConnectionRoot(string $path = '') throw new RuntimeException('Root is invalid or does not exist: ' . $base_path); } - // Store absolute path for further reference. ftp_pwd($this->connection); } @@ -207,14 +199,14 @@ public function getCurrentDirectory() /** * Store directly the upload file * - * @param UploadFile $file + * @param UploadedFile $file * @param string $location * @param array $option * - * @return mixed + * @return array|bool|string * @throws InvalidArgumentException */ - public function store(UploadFile $file, ?string $location = null, array $option = []): array|bool + public function store(UploadedFile $file, ?string $location = null, array $option = []): array|bool|string { if (is_null($location)) { throw new InvalidArgumentException("Please define the store location"); @@ -232,7 +224,7 @@ public function store(UploadFile $file, ?string $location = null, array $option rewind($stream); // - $result = $this->writeStream($location, $stream, $option); + $result = $this->writeStream($location, $stream); fclose($stream); if ($result === false) { @@ -358,14 +350,14 @@ public function makeDirectory(string $dirname, int $mode = 0777): bool $directories = explode('/', $dirname); foreach ($directories as $directory) { - if (false === $this->makeActualDirectory($directory, $mode)) { - $this->setConnectionRoot(); + if (false === $this->makeActualDirectory($directory)) { + $this->changePath(); return false; } ftp_chdir($connection, $directory); } - $this->setConnectionRoot(); + $this->changePath(); return true; } @@ -405,7 +397,7 @@ protected function makeActualDirectory(string $directory): bool public function get(string $filename): ?string { if (!$stream = $this->readStream($filename)) { - return false; + return null; } $contents = stream_get_contents($stream); @@ -486,13 +478,18 @@ public function isFile(string $filename): bool */ public function isDirectory(string $dirname): bool { - $listing = $this->listDirectoryContents(); + $original_directory = ftp_pwd($this->connection); - $dirname_info = array_filter($listing, function ($item) use ($dirname) { - return $item['type'] === 'directory' && $item['name'] === $dirname; - }); + // Test if you can change directory to $dirname + // suppress errors in case $dir is not a file or not a directory + if (!@ftp_chdir($this->connection, $dirname)) { + return false; + } - return count($dirname_info) !== 0; + // If it is a directory, then change the directory back to the original directory + ftp_chdir($this->connection, $original_directory); + + return true; } /** @@ -567,7 +564,7 @@ protected function listDirectoryContents($directory = '.') $listing = @ftp_rawlist($this->getConnection(), '.') ?: []; - $this->setConnectionRoot(); + $this->changePath(); return $this->normalizeDirectoryListing($listing); } @@ -638,8 +635,10 @@ private function readStream(string $path): mixed * * @throws RuntimeException */ - private function setConnectionPassiveMode() + private function activePassiveMode() { + @ftp_set_option($this->connection, FTP_USEPASVADDRESS, false); + if (!ftp_pasv($this->connection, $this->use_passive_mode)) { throw new RuntimeException( 'Could not set passive mode for connection: ' diff --git a/src/Storage/Service/S3Service.php b/src/Storage/Service/S3Service.php index 80ef84bb..fd0f334f 100644 --- a/src/Storage/Service/S3Service.php +++ b/src/Storage/Service/S3Service.php @@ -5,7 +5,7 @@ namespace Bow\Storage\Service; use Aws\S3\S3Client; -use Bow\Http\UploadFile; +use Bow\Http\UploadedFile; use Bow\Storage\Contracts\ServiceInterface; class S3Service implements ServiceInterface @@ -73,14 +73,12 @@ public static function getInstance(): S3Service /** * Function to upload a file * - * @param UploadFile $file + * @param UploadedFile $file * @param string $location * @param array $option - * - * @return mixed - * @throws InvalidArgumentException + * @return array|bool|string */ - public function store(UploadFile $file, ?string $location = null, array $option = []): array|bool + public function store(UploadedFile $file, ?string $location = null, array $option = []): array|bool|string { $result = $this->put($file->getHashName(), $file->getContent()); @@ -127,22 +125,22 @@ public function prepend(string $filename, string $content): bool * @param string $content * @param array $options * - * @return bool + * @return mixed */ - public function put(string $file, string $content, array $options = []): bool + public function put(string $file, string $content, array $options = []): mixed { $options = is_string($options) ? ['visibility' => $options] : (array) $options; - $this->client->putObject([ + $result = $this->client->putObject([ 'Bucket' => $this->config['bucket'], 'Key' => $file, 'Body' => $content, "Visibility" => $options["visibility"] ?? 'public' ]); - return true; + return $result; } /** @@ -221,9 +219,9 @@ public function makeDirectory(string $bucket, int $mode = 0777, array $option = * Recover the contents of the file * * @param string $filename - * @return null|string + * @return ?string */ - public function get(string $filename): string + public function get(string $filename): ?string { $result = $this->client->getObject([ 'Bucket' => $this->config['bucket'], diff --git a/src/Storage/Storage.php b/src/Storage/Storage.php index cd3923b4..b401b8e1 100644 --- a/src/Storage/Storage.php +++ b/src/Storage/Storage.php @@ -5,6 +5,7 @@ namespace Bow\Storage; use BadMethodCallException; +use Bow\Storage\Contracts\FilesystemInterface; use InvalidArgumentException; use Bow\Storage\Exception\DiskNotFoundException; use Bow\Storage\Exception\ServiceConfigurationNotFoundException; @@ -12,6 +13,7 @@ use Bow\Storage\Service\DiskFilesystemService; use Bow\Storage\Service\FTPService; use Bow\Storage\Service\S3Service; +use ErrorException; class Storage { @@ -47,7 +49,7 @@ class Storage * @return DiskFilesystemService * @throws DiskNotFoundException */ - public static function disk(?string $disk = null) + public static function disk(?string $disk = null): DiskFilesystemService { // Use the default disk as fallback if (is_null($disk)) { @@ -72,6 +74,8 @@ public static function disk(?string $disk = null) * * @param string $service * @return FTPService|S3Service + * @throws ServiceConfigurationNotFoundException + * @throws ServiceNotFoundException */ public static function service(string $service) { @@ -90,14 +94,14 @@ public static function service(string $service) throw (new ServiceNotFoundException(sprintf( '"%s" driver is not support.', $driver - )))->setService($driver); + )))->setService($service); } if (!array_key_exists($driver, self::$available_services_driviers)) { throw (new ServiceNotFoundException(sprintf( '"%s" is not registered as a service.', $driver - )))->setService($driver); + )))->setService($service); } $service_class = static::$available_services_driviers[$driver]; @@ -126,10 +130,10 @@ public static function pushService(array $drivers) * Configure Storage * * @param array $config - * @return MountFilesystem + * @return FilesystemInterface * @throws */ - public static function configure(array $config) + public static function configure(array $config): FilesystemInterface { static::$config = $config; @@ -149,6 +153,12 @@ public static function configure(array $config) */ public function __call($name, array $arguments) { + if (is_null(static::$disk)) { + throw new ErrorException( + "Unable to get storage instance before configuration" + ); + } + if (method_exists(static::$disk, $name)) { return call_user_func_array([static::$disk, $name], $arguments); } @@ -165,10 +175,18 @@ public function __call($name, array $arguments) */ public static function __callStatic($name, array $arguments) { + if (is_null(static::$disk)) { + throw new ErrorException( + "Unable to get storage instance before configuration" + ); + } + if (method_exists(static::$disk, $name)) { return call_user_func_array([static::$disk, $name], $arguments); } - throw new BadMethodCallException("unkdown $name method"); + throw new BadMethodCallException( + "The method $name is not defined" + ); } } diff --git a/src/Support/Arraydotify.php b/src/Support/Arraydotify.php index 71418415..3bc1522c 100644 --- a/src/Support/Arraydotify.php +++ b/src/Support/Arraydotify.php @@ -126,6 +126,14 @@ private function find(array $origin, string $segment): ?array foreach ($parts as $key => $part) { if ($key != 0) { + if (is_array($array) && is_null($array[$part] ?? null)) { + return null; + } + + if (is_array($array) && isset($array[$part]) && is_null($array[$part])) { + return null; + } + if (isset($array[$part]) && is_array($array[$part])) { $array = &$array[$part]; } @@ -133,7 +141,7 @@ private function find(array $origin, string $segment): ?array continue; } - if (!isset($origin[$part])) { + if (!isset($origin[$part]) || is_null($origin[$part])) { return null; } @@ -182,6 +190,8 @@ public function offsetSet($offset, $value): void { $this->items[$offset] = $value; + $this->items = $this->dotify($this->items); + $this->updateOrigin(); } @@ -192,6 +202,8 @@ public function offsetUnset($offset): void { unset($this->items[$offset]); + $this->items = $this->dotify($this->items); + $this->updateOrigin(); } } diff --git a/src/Support/Collection.php b/src/Support/Collection.php index d8840a89..b0c20135 100644 --- a/src/Support/Collection.php +++ b/src/Support/Collection.php @@ -52,11 +52,11 @@ public function last(): mixed /** * Check existence of a key in the session collection * - * @param string $key + * @param int|string $key * @param bool $strict * @return bool */ - public function has(string $key, bool $strict = false): bool + public function has(int|string $key, bool $strict = false): bool { // When $strict is true, he check $key not how a key but a value. $isset = isset($this->storage[$key]); @@ -93,11 +93,11 @@ public function isEmpty(): bool /** * Allows to recover a value or value collection. * - * @param string $key + * @param int|string $key * @param mixed $default * @return mixed */ - public function get(string $key = null, mixed $default = null) + public function get(int|string $key = null, mixed $default = null) { if (is_null($key)) { return $this->storage; @@ -162,13 +162,24 @@ public function count(): int return count($this->storage); } + /** + * Chunk the storage content + * + * @param int $count + * @return int + */ + public function chunk(int $chunk): Collection + { + return new Collection(array_chunk($this->storage, $chunk)); + } + /** * To retrieve a value or value collection form d'instance de collection. * * @param string $key * @return Collection */ - public function collectionify(string $key): Collection + public function collectify(string $key): Collection { $data = []; @@ -695,7 +706,7 @@ public function __toString() /** * jsonSerialize * - * @return string + * @return mixed */ public function jsonSerialize(): mixed { diff --git a/src/Support/Env.php b/src/Support/Env.php index 552f125b..a2f7ad2b 100644 --- a/src/Support/Env.php +++ b/src/Support/Env.php @@ -4,6 +4,8 @@ namespace Bow\Support; +use Bow\Application\Exception\ApplicationException; + class Env { /** @@ -52,11 +54,20 @@ public static function load(string $filename) // Get the env file content $content = file_get_contents($filename); - static::$envs = json_decode(trim($content), true); + $envs = json_decode(trim($content), true, 1024); + + if (json_last_error()) { + throw new ApplicationException( + json_last_error_msg() . ": check your env json and synthax please." + ); + } + + static::$envs = $envs; + static::$envs = static::bindVariables($envs); foreach (static::$envs as $key => $value) { $key = Str::upper(trim($key)); - putenv($key . '=' . $value); + putenv($key . '=' . json_encode($value)); } if (json_last_error() == JSON_ERROR_SYNTAX) { @@ -90,7 +101,13 @@ public static function get(string $key, mixed $default = null): mixed return $default; } - return $value; + if (!is_string($value)) { + return $value; + } + + $data = json_decode($value); + + return json_last_error() ? $value : $data; } /** @@ -103,8 +120,38 @@ public static function get(string $key, mixed $default = null): mixed public static function set(string $key, mixed $value): bool { $key = Str::upper(trim($key)); + static::$envs[$key] = $value; return putenv($key . '=' . $value); } + + /** + * Bind variable + * + * @param array $envs + * @return array + */ + private static function bindVariables(array $envs): array + { + $keys = array_keys(static::$envs); + + foreach ($envs as $env_key => $value) { + foreach ($keys as $key) { + if ($key == $env_key) { + break; + } + if (is_array($value)) { + $envs[$env_key] = static::bindVariables($value); + break; + } + if (is_string($value) && preg_match("/\\$\{\s*$key\s*\}/", $value)) { + $envs[$env_key] = str_replace('${' . $key . '}', static::$envs[$key], $value); + break; + } + } + } + + return $envs; + } } diff --git a/src/Support/Log.php b/src/Support/Log.php new file mode 100644 index 00000000..568ebac1 --- /dev/null +++ b/src/Support/Log.php @@ -0,0 +1,26 @@ +error($message, $context); + } + + /** + * Logger service + * + * @param string $message + * @param array $context + * @return mixed + */ + public function info(string $message, array $context = []) + { + app('logger')->info($message, $context); + } + + /** + * Logger service + * + * @param string $message + * @param array $context + * @return mixed + */ + public function warning(string $message, array $context = []) + { + app('logger')->warning($message, $context); + } + + /** + * Logger service + * + * @param string $message + * @param array $context + * @return mixed + */ + public function alert(string $message, array $context = []) + { + app('logger')->alert($message, $context); + } + + /** + * Logger service + * + * @param string $message + * @param array $context + * @return mixed + */ + public function critical(string $message, array $context = []) + { + app('logger')->critical($message, $context); + } + + /** + * Logger service + * + * @param string $message + * @param array $context + * @return mixed + */ + public function emergency(string $message, array $context = []) + { + app('logger')->emergency($message, $context); + } +} diff --git a/src/Queue/Traits/SerializesModels.php b/src/Support/Serializes.php similarity index 90% rename from src/Queue/Traits/SerializesModels.php rename to src/Support/Serializes.php index 5d895c31..b9e5506a 100644 --- a/src/Queue/Traits/SerializesModels.php +++ b/src/Support/Serializes.php @@ -1,11 +1,11 @@ setAccessible(true); - - if (! $property->isInitialized($this)) { + if (!$property->isInitialized($this)) { continue; } $value = $this->getPropertyValue($property); - if ($property->hasDefaultValue() && $value === $property->getDefaultValue()) { continue; } @@ -76,7 +74,7 @@ public function __unserialize(array $values) $name = "\0*\0{$name}"; } - if (! array_key_exists($name, $values)) { + if (!array_key_exists($name, $values)) { continue; } @@ -95,8 +93,9 @@ public function __unserialize(array $values) * @param \ReflectionProperty $property * @return mixed */ - protected function getPropertyValue(ReflectionProperty $property) - { + protected function getPropertyValue( + ReflectionProperty $property + ) { $property->setAccessible(true); return $property->getValue($this); diff --git a/src/Support/Str.php b/src/Support/Str.php index c887c599..ab6c7c50 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -6,6 +6,7 @@ use ErrorException; use ForceUTF8\Encoding; +use Ramsey\Uuid\Uuid; class Str { @@ -100,21 +101,21 @@ public static function plurial(string $str): string * slice * * @param string $str - * @param string $start - * @param string|null $end + * @param int $start + * @param int $length * @return string */ - public static function slice(string $str, int $start, ?int $end = null) + public static function slice(string $str, int $start, ?int $length = null) { $sliceStr = ''; if (is_string($str)) { - if ($end === null) { - $end = static::len($str); + if ($length === null) { + $length = static::len($str); } - if ($start < $end) { - $sliceStr = mb_substr($str, $start, $end, 'UTF-8'); + if ($start < $length) { + $sliceStr = mb_substr($str, $start, $length, 'UTF-8'); } } @@ -129,7 +130,7 @@ public static function slice(string $str, int $start, ?int $end = null) * @param int|null $limit * @return array */ - public static function split(string $pattern, string $str, ?string $limit = null): array + public static function split(string $pattern, string $str, ?int $limit = null): array { return mb_split($pattern, $str, $limit); } @@ -228,11 +229,21 @@ public static function repeat(string $str, int $number): string * @param int $size * @return string */ - public static function randomize(int $size = 16): string + public static function random(int $size = 16): string { return static::slice(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, $size); } + /** + * Get rondom uuid + * + * @return string + */ + public static function uuid(): string + { + return Uuid::uuid4()->toString(); + } + /** * slugify slug creator using a simple chain. * eg: 'I am a string of character' => 'i-am-a-chain-of-character' @@ -252,6 +263,18 @@ public static function slugify(string $str, string $delimiter = '-'): string return preg_replace('/-{2,}/', $delimiter, $temp); } + /** + * Alias of slugify + * + * @param string $str + * @param string $delimiter + * @return string + */ + public static function slug(string $str, string $delimiter = '-'): string + { + return static::slugify($str, $delimiter); + } + /** * unslugify, Lets you undo a slug * @@ -263,6 +286,17 @@ public static function unSlugify(string $str): string return preg_replace('/[^a-z0-9]/', ' ', strtolower(trim(strip_tags($str)))); } + /** + * Alias of unslugify + * + * @param string $str + * @return string + */ + public static function unSlug(string $str): string + { + return static::unSlugify($str); + } + /** * Check if the email is a valid email. * @@ -409,13 +443,17 @@ public static function count(string $pattern, string $str): int * @param int $len * @return string */ - public static function getWords(string $words, int $len): string + public static function words(string $words, int $len): string { $wordParts = explode(' ', $words); $sentence = ''; for ($i = 0; $i < $len; $i++) { + if (!isset($wordParts[$i])) { + break; + } + $sentence .= ' ' . $wordParts[$i]; } diff --git a/src/Support/helpers.php b/src/Support/helpers.php index 0758347b..496de7bf 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -2,31 +2,47 @@ use Bow\Auth\Auth; use Bow\Mail\Mail; +use Bow\View\View; +use Carbon\Carbon; +use Monolog\Logger; use Bow\Event\Event; use Bow\Support\Env; +use Bow\Support\Str; +use Bow\Http\Request; use Bow\Support\Util; +use Bow\Http\Redirect; +use Bow\Http\Response; use Bow\Security\Hash; use Bow\Session\Cookie; +use Bow\Http\HttpStatus; +use Bow\Security\Crypto; use Bow\Session\Session; use Bow\Storage\Storage; use Bow\Container\Capsule; +use Bow\Security\Sanitize; use Bow\Security\Tokenize; use Bow\Support\Collection; +use Bow\Validation\Validate; +use Bow\Database\Barry\Model; use Bow\Translate\Translator; +use Bow\Validation\Validator; use Bow\Queue\ProducerService; use Bow\Database\Database as DB; +use Bow\Auth\Guards\GuardContract; use Bow\Http\Exception\HttpException; -use Bow\Http\Redirect; +use Bow\Mail\Contracts\MailDriverInterface; +use Bow\Storage\Exception\ResourceException; +use Bow\Storage\Service\DiskFilesystemService; if (!function_exists('app')) { /** * Application container * - * @param string|null $key + * @param ?string $key * @param array $setting - * @return \Bow\Support\Capsule|mixed + * @return mixed */ - function app($key = null, array $setting = []) + function app(?string $key = null, array $setting = []): mixed { $capsule = Capsule::getInstance(); @@ -51,7 +67,7 @@ function app($key = null, array $setting = []) * @return \Bow\Configuration\Loader|mixed * @throws */ - function config($key = null, $setting = null) + function config($key = null, $setting = null): mixed { $config = \Bow\Configuration\Loader::getInstance(); @@ -71,11 +87,16 @@ function config($key = null, $setting = null) /** * Response object instance * - * @return \Bow\Http\Response + * @return Response */ - function response() + function response(): Response { - return app('response'); + /** + * @var Response + */ + $response = app('response'); + + return $response; } } @@ -83,11 +104,16 @@ function response() /** * Represents the Request class * - * @return \Bow\Http\Request + * @return Request */ - function request() + function request(): Request { - return app('request'); + /** + * @var Request + */ + $request = app('request'); + + return $request; } } @@ -134,7 +160,7 @@ function db(string $name = null, callable $cb = null) * @param int $code * @return mixed */ - function view(string $template, $data = [], $code = 200) + function view(string $template, int|array $data = [], int $code = 200) { if (is_int($data)) { $code = $data; @@ -145,7 +171,7 @@ function view(string $template, $data = [], $code = 200) response() ->status($code); - return Bow\View\View::parse($template, $data); + return View::parse($template, $data); } } @@ -156,6 +182,7 @@ function view(string $template, $data = [], $code = 200) * @param string $name * @param string $connexion * @return Bow\Database\QueryBuilder + * @deprecated */ function table(string $name, string $connexion = null) { @@ -181,6 +208,24 @@ function get_last_insert_id(string $name = null) } } +if (!function_exists('db_table')) { + /** + * Table alias of DB::table + * + * @param string $name + * @param string $connexion + * @return Bow\Database\QueryBuilder + */ + function db_table(string $name, string $connexion = null) + { + if (is_string($connexion)) { + db($connexion); + } + + return DB::table($name); + } +} + if (!function_exists('db_select')) { /** * Launches SELECT SQL Queries @@ -315,7 +360,14 @@ function create_csrf_token(int $time = null): ?array */ function csrf_token(): string { - $csrf = create_csrf_token(); + $csrf = (array) create_csrf_token(); + + if (count($csrf) == 0) { + throw new HttpException( + "CSRF token is not generated", + 500 + ); + } return $csrf['token']; } @@ -329,7 +381,14 @@ function csrf_token(): string */ function csrf_field(): string { - $csrf = create_csrf_token(); + $csrf = (array) create_csrf_token(); + + if (count($csrf) == 0) { + throw new HttpException( + "CSRF token is not generated", + 500 + ); + } return $csrf['field']; } @@ -445,7 +504,7 @@ function sanitize(mixed $data): mixed return $data; } - return \Bow\Security\Sanitize::make($data); + return Sanitize::make($data); } } @@ -462,7 +521,7 @@ function secure(mixed $data): mixed return $data; } - return \Bow\Security\Sanitize::make($data, true); + return Sanitize::make($data, true); } } @@ -502,7 +561,7 @@ function get_header(string $key): ?string */ function redirect(string $path = null): Redirect { - $redirect = \Bow\Http\Redirect::getInstance(); + $redirect = Redirect::getInstance(); if ($path !== null) { $redirect->to($path); @@ -512,47 +571,6 @@ function redirect(string $path = null): Redirect } } -if (!function_exists('curl')) { - /** - * Curl help - * - * @param string $method - * @param string $url - * @param array $payload - * @param bool $return - * @param array $headers - * @return array|null - */ - function curl(string $method, string $url, array $payload = [], array $headers = [], $return = false) - { - $method = strtoupper($method); - $options = []; - - if (!in_array($method, ['GET'])) { - $options['CURLOPT_POSTFIELDS'] = http_build_query($payload); - $options['CURLOPT_POST'] = 1; - } - - $ch = curl_init($url); - - if ($return == true) { - if (!curl_setopt($ch, CURLOPT_RETURNTRANSFER, true)) { - curl_close($ch); - return null; - } - } - - // Set curl option - curl_setopt_array($ch, $options); - - // Execute curl - $data = curl_exec($ch); - curl_close($ch); - - return $data; - } -} - if (!function_exists('url')) { /** * Build url @@ -616,9 +634,9 @@ function set_pdo(PDO $pdo): PDO * Create new Ccollection instance * * @param array $data - * @return \Bow\Support\Collection + * @return Collection */ - function collect(array $data = []): \Bow\Support\Collection + function collect(array $data = []): Collection { return new Collection($data); } @@ -633,7 +651,7 @@ function collect(array $data = []): \Bow\Support\Collection */ function encrypt(string $data): string { - return \Bow\Security\Crypto::encrypt($data); + return Crypto::encrypt($data); } } @@ -646,7 +664,7 @@ function encrypt(string $data): string */ function decrypt(string $data): string { - return \Bow\Security\Crypto::decrypt($data); + return Crypto::decrypt($data); } } @@ -659,7 +677,7 @@ function decrypt(string $data): string */ function db_transaction(callable $cb = null): void { - DB::startTransaction($cb); + DB::startTransaction(); } } @@ -671,7 +689,7 @@ function db_transaction(callable $cb = null): void */ function db_transaction_started(): bool { - return DB::getPdo()->inTransaction(); + return DB::inTransaction(); } } @@ -741,14 +759,14 @@ function flash(string $key, string $message) * @param null|string $view * @param array $data * @param callable $cb - * @return \Bow\Mail\Contracts\MailDriverInterface|bool + * @return MailDriverInterface|bool * @throws */ function email( string $view = null, array $data = [], callable $cb = null - ): \Bow\Mail\Contracts\MailDriverInterface|bool { + ): MailDriverInterface|bool { if ($view === null) { return Mail::getInstance(); } @@ -777,18 +795,19 @@ function raw_email(string $to, string $subject, string $message, array $headers /** * Session help * - * @param string $value - * @param mixed $default + * @param array|string $value + * @param mixed $default * @return mixed */ - function session(string $value = null, mixed $default = null): mixed + function session(array|string $value = null, mixed $default = null): mixed { if ($value == null) { return Session::getInstance(); } if (!is_array($value)) { - return Session::getInstance()->get($value, $default); + $key = $value; + return Session::getInstance()->get($key, $default); } foreach ($value as $key => $item) { @@ -815,11 +834,7 @@ function session(string $value = null, mixed $default = null): mixed function cookie( string $key = null, mixed $data = null, - int $expirate = 3600, - string $path = null, - string $domain = null, - bool $secure = false, - bool $http = true + int $expirate = 3600 ) { if ($key === null) { return Cookie::all(); @@ -830,7 +845,7 @@ function cookie( } if ($key !== null && $data !== null) { - return Cookie::set($key, $data, $expirate, $path, $domain, $secure, $http); + return Cookie::set($key, $data, $expirate); } return null; @@ -843,11 +858,12 @@ function cookie( * * @param array $inputs * @param array $rules - * @return \Bow\Validation\Validate + * @param array $messages + * @return Validate */ - function validator(array $inputs, array $rules): \Bow\Validation\Validate + function validator(array $inputs, array $rules, array $messages = []): Validate { - return \Bow\Validation\Validator::make($inputs, $rules); + return Validator::make($inputs, $rules, $messages); } } @@ -856,40 +872,36 @@ function validator(array $inputs, array $rules): \Bow\Validation\Validate * Get Route by name * * @param string $name - * @param array $data + * @param bool|array $data * @param bool $absolute * @return string */ - function route(string $name, array $data = [], $absolute = false) + function route(string $name, bool|array $data = [], bool $absolute = false) { - $routes = config('app.routes'); - if (is_bool($data)) { $absolute = $data; $data = []; } - if (!isset($routes[$name])) { + $url = config('app.routes.' . $name); + + if (is_null($url)) { throw new \InvalidArgumentException( 'The route named ' . $name . ' does not define.', E_USER_ERROR ); } - $url = $routes[$name]; - - if (preg_match_all('/(?::([a-zA-Z0-9_-]+\??))/', $url, $matches)) { + if (preg_match_all('/(?::([a-zA-Z0-9_]+\??))/', $url, $matches)) { $keys = end($matches); foreach ($keys as $key) { - if (preg_match("/?$/", $key)) { + if (preg_match("/\?$/", $key)) { $valide_key = trim($key, "?"); $value = $data[$valide_key] ?? ""; unset($data[$valide_key]); } else { - if (isset($data[$key])) { - throw new InvalidArgumentException( - "The $key key is not provide" - ); + if (!isset($data[$key])) { + throw new InvalidArgumentException("Route: The $key key is not provide"); } $value = $data[$key]; unset($data[$key]); @@ -917,12 +929,12 @@ function route(string $name, array $data = [], $absolute = false) /** * Escape the HTML tags in the chain. * - * @param string $value + * @param ?string $value * @return string */ - function e(string $value): string + function e(?string $value = null): string { - return htmlentities($value, ENT_QUOTES, 'UTF-8', false); + return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8'); } } @@ -938,31 +950,17 @@ function storage_service(string $service) } } -if (!function_exists('mount')) { +if (!function_exists('app_file_system')) { /** * Alias on the mount method * - * @param string $mount - * @return \Bow\Storage\Service\DiskFilesystemService - * @throws \Bow\Storage\Exception\ResourceException + * @param string $disk + * @return DiskFilesystemService + * @throws ResourceException */ - function mount(string $mount): \Bow\Storage\Service\DiskFilesystemService + function app_file_system(string $disk): DiskFilesystemService { - return Storage::mount($mount); - } -} - -if (!function_exists('file_system')) { - /** - * Alias on the mount method - * - * @param string $mount - * @return \Bow\Storage\Service\DiskFilesystemService - * @throws \Bow\Storage\Exception\ResourceException - */ - function file_system(string $mount): \Bow\Storage\Service\DiskFilesystemService - { - return mount($mount); + return Storage::disk($disk); } } @@ -970,13 +968,17 @@ function file_system(string $mount): \Bow\Storage\Service\DiskFilesystemService /** * Cache help * - * @param string $key - * @param mixed $value - * @param int $ttl + * @param ?string $key + * @param ?mixed $value + * @param ?int $ttl * @return mixed */ function cache(string $key = null, mixed $value = null, int $ttl = null) { + if ($key === null) { + return \Bow\Cache\Cache::getInstance(); + } + if ($key !== null && $value === null) { return \Bow\Cache\Cache::get($key); } @@ -990,14 +992,26 @@ function cache(string $key = null, mixed $value = null, int $ttl = null) * Make redirection to back * * @param int $status - * @return Bow\Http\Redirect + * @return Redirect */ - function redirect_back(int $status = 302): \Bow\Http\Redirect + function redirect_back(int $status = 302): Redirect { return redirect()->back($status); } } +if (!function_exists('app_now')) { + /** + * Get the current carbon + * + * @return Carbon + */ + function app_now(): Carbon + { + return Carbon::now(); + } +} + if (!function_exists('app_hash')) { /** * Alias on the class Hash. @@ -1031,20 +1045,20 @@ function bow_hash(string $data, string $hash_value = null): bool|string } } -if (!function_exists('trans')) { +if (!function_exists('app_trans')) { /** * Make translation * * @param string $key * @param array $data * @param bool $choose - * @return string|Bow\Translate\Translator + * @return string|Translator */ - function trans( + function app_trans( string $key = null, array $data = [], bool $choose = false - ): string|Bow\Translate\Translator { + ): string|Translator { if (is_null($key)) { return Translator::getInstance(); } @@ -1071,8 +1085,8 @@ function t( string $key, array $data = [], bool $choose = false - ): string|Bow\Translate\Translator { - return trans($key, $data, $choose); + ): string|Translator { + return app_trans($key, $data, $choose); } } @@ -1089,8 +1103,8 @@ function __( string $key, array $data = [], bool $choose = false - ): string|Bow\Translate\Translator { - return trans($key, $data, $choose); + ): string|Translator { + return app_trans($key, $data, $choose); } } @@ -1131,11 +1145,15 @@ function app_assets(string $filename): string * * @param int $code * @param string $message - * @return \Bow\Http\Response + * @return Response * @throws HttpException */ function app_abort(int $code = 500, string $message = '') { + if (strlen($message) == 0) { + $message = HttpStatus::getMessage($code); + } + throw new HttpException($message, $code); } } @@ -1147,13 +1165,13 @@ function app_abort(int $code = 500, string $message = '') * @param boolean $boolean * @param int $code * @param string $message - * @return \Bow\Http\Response|null + * @return Response|null */ function app_abort_if( bool $boolean, int $code, string $message = '' - ): \Bow\Http\Response|null { + ): Response|null { if ($boolean) { return app_abort($code, $message); } @@ -1174,6 +1192,18 @@ function app_mode(): string } } +if (!function_exists('app_in_debug')) { + /** + * Get app enviroment mode + * + * @return bool + */ + function app_in_debug(): bool + { + return (bool) app_env('APP_DEBUG'); + } +} + if (!function_exists('client_locale')) { /** * Get client request language @@ -1205,10 +1235,10 @@ function old(string $key, mixed $fullback = null): mixed * Recovery of the guard * * @param string $guard - * @return \Bow\Auth\Guards\GuardContract + * @return GuardContract * @throws */ - function auth(string $guard = null): \Bow\Auth\Guards\GuardContract + function auth(string $guard = null): GuardContract { $auth = Auth::getInstance(); @@ -1224,22 +1254,11 @@ function auth(string $guard = null): \Bow\Auth\Guards\GuardContract /** * Log error message * - * @param string $level - * @param string $message - * @param array $context - * @return Monolog + * @return Logger */ - function logger(?string $level, ?string $message, array $context = []) + function logger(): Logger { - if (is_null($level)) { - return app('logger'); - } - - if (!in_array($level, ['info', 'warning', 'error', 'critical', 'debug'])) { - return false; - } - - return app('logger')->$level($message, $context); + return app('logger'); } } @@ -1253,7 +1272,7 @@ function logger(?string $level, ?string $message, array $context = []) */ function str_slug(string $str, string $sep = '-'): string { - return \Bow\Support\Str::slugify($str, $sep); + return Str::slugify($str, $sep); } } @@ -1266,7 +1285,19 @@ function str_slug(string $str, string $sep = '-'): string */ function str_is_mail(string $email): bool { - return \Bow\Support\Str::isMail($email); + return Str::isMail($email); + } +} + +if (!function_exists('str_uuid')) { + /** + * Get str uuid + * + * @return string + */ + function str_uuid(): string + { + return Str::uuid(); } } @@ -1280,7 +1311,7 @@ function str_is_mail(string $email): bool */ function str_is_domain(string $domain): bool { - return \Bow\Support\Str::isDomain($domain); + return Str::isDomain($domain); } } @@ -1294,7 +1325,7 @@ function str_is_domain(string $domain): bool */ function str_is_slug(string $slug): string { - return \Bow\Support\Str::isSlug($slug); + return Str::isSlug($slug); } } @@ -1308,7 +1339,7 @@ function str_is_slug(string $slug): string */ function str_is_alpha(string $string): bool { - return \Bow\Support\Str::isAlpha($string); + return Str::isAlpha($string); } } @@ -1321,7 +1352,7 @@ function str_is_alpha(string $string): bool */ function str_is_lower(string $string): bool { - return \Bow\Support\Str::isLower($string); + return Str::isLower($string); } } @@ -1334,7 +1365,7 @@ function str_is_lower(string $string): bool */ function str_is_upper(string $string): bool { - return \Bow\Support\Str::isUpper($string); + return Str::isUpper($string); } } @@ -1348,7 +1379,7 @@ function str_is_upper(string $string): bool */ function str_is_alpha_num(string $slug): bool { - return \Bow\Support\Str::isAlphaNum($slug); + return Str::isAlphaNum($slug); } } @@ -1361,7 +1392,7 @@ function str_is_alpha_num(string $slug): bool */ function str_shuffle_words(string $words): string { - return \Bow\Support\Str::shuffleWords($words); + return Str::shuffleWords($words); } } @@ -1375,7 +1406,7 @@ function str_shuffle_words(string $words): string */ function str_wordify(string $words, string $sep = ''): array { - return \Bow\Support\Str::wordify($words, $sep); + return Str::wordify($words, $sep); } } @@ -1388,7 +1419,7 @@ function str_wordify(string $words, string $sep = ''): array */ function str_plurial(string $slug): string { - return \Bow\Support\Str::plurial($slug); + return Str::plurial($slug); } } @@ -1401,7 +1432,7 @@ function str_plurial(string $slug): string */ function str_camel($slug): string { - return \Bow\Support\Str::camel($slug); + return Str::camel($slug); } } @@ -1414,7 +1445,7 @@ function str_camel($slug): string */ function str_snake(string $slug): string { - return \Bow\Support\Str::snake($slug); + return Str::snake($slug); } } @@ -1428,7 +1459,7 @@ function str_snake(string $slug): string */ function str_contains(string $search, string $string): bool { - return \Bow\Support\Str::contains($search, $string); + return Str::contains($search, $string); } } @@ -1441,7 +1472,7 @@ function str_contains(string $search, string $string): bool */ function str_capitalize(string $slug): string { - return \Bow\Support\Str::capitalize($slug); + return Str::capitalize($slug); } } @@ -1454,7 +1485,7 @@ function str_capitalize(string $slug): string */ function str_random(string $string): string { - return \Bow\Support\Str::randomize($string); + return Str::random($string); } } @@ -1466,7 +1497,7 @@ function str_random(string $string): string */ function str_force_in_utf8(): void { - \Bow\Support\Str::forceInUTF8(); + Str::forceInUTF8(); } } @@ -1479,7 +1510,7 @@ function str_force_in_utf8(): void */ function str_fix_utf8(string $string): string { - return \Bow\Support\Str::fixUTF8($string); + return Str::fixUTF8($string); } } @@ -1495,13 +1526,14 @@ function db_seed(string $name, array $data = []): mixed { if (class_exists($name, true)) { $instance = app($name); - if ($instance instanceof \Bow\Database\Barry\Model) { + + if ($instance instanceof Model) { $table = $instance->getTable(); return DB::table($table)->insert($data); } } - $filename = rtrim(config('app.seeder_path'), '/') . '/' . $name . '_seeder.php'; + $filename = rtrim(config('app.seeder_path'), '/') . '/' . $name . '.php'; if (!file_exists($filename)) { throw new \ErrorException('[' . $name . '] seeder file not found'); @@ -1514,7 +1546,7 @@ function db_seed(string $name, array $data = []): mixed foreach ($seeds as $table => $payload) { if (class_exists($table, true)) { $instance = app($table); - if ($instance instanceof \Bow\Database\Barry\Model) { + if ($instance instanceof Model) { $table = $instance->getTable(); } } diff --git a/src/Testing/Features/SeedingHelper.php b/src/Testing/Features/SeedingHelper.php index 0031cac4..557db6fc 100644 --- a/src/Testing/Features/SeedingHelper.php +++ b/src/Testing/Features/SeedingHelper.php @@ -15,6 +15,6 @@ trait SeedingHelper */ public function seed(string $seeder, array $data = []): int { - return seed($seeder, $data); + return db_seed($seeder, $data); } } diff --git a/src/Testing/KernelTesting.php b/src/Testing/KernelTesting.php index 50cf3bea..9badc418 100644 --- a/src/Testing/KernelTesting.php +++ b/src/Testing/KernelTesting.php @@ -6,9 +6,9 @@ class KernelTesting extends ConfigurationLoader { - public static array $configurations = []; - public static array $events = []; - public static array $middlewares = []; + private static array $configurations = []; + private static array $events = []; + private static array $middlewares = []; /** * Set the loading configuration @@ -16,7 +16,7 @@ class KernelTesting extends ConfigurationLoader * @param array $configurations * @return void */ - public static function withConfiguations(array $configurations): void + public static function withConfigurations(array $configurations): void { static::$configurations = $configurations; } diff --git a/src/Testing/Response.php b/src/Testing/Response.php index f7e57ccc..c91a08f1 100644 --- a/src/Testing/Response.php +++ b/src/Testing/Response.php @@ -5,19 +5,19 @@ namespace Bow\Testing; use InvalidArgumentException; -use Bow\Http\Client\Parser; +use Bow\Http\Client\Response as HttpClientResponse; class Response { /** - * The http parser + * The http http_response * - * @var Parser + * @var HttpClientResponse */ - private Parser $parser; + private HttpClientResponse $http_response; /** - * The parser content + * The http_response content * * @var string */ @@ -26,13 +26,13 @@ class Response /** * Behovior constructor. * - * @param Parser $parser + * @param HttpClientResponse $http_response */ - public function __construct(Parser $parser) + public function __construct(HttpClientResponse $http_response) { - $this->parser = $parser; + $this->http_response = $http_response; - $this->content = $parser->getContent(); + $this->content = $http_response->getContent(); } /** @@ -93,7 +93,7 @@ public function assertContainsExactText(string $data, string $message = ''): Res */ public function assertHeader(string $header, string $message = ''): Response { - Assert::assertArrayHasKey($header, $this->parser->getHeaders(), $message); + Assert::assertArrayHasKey($header, $this->http_response->getHeaders(), $message); return $this; } @@ -107,7 +107,7 @@ public function assertHeader(string $header, string $message = ''): Response */ public function assertArray(string $message = ''): Response { - Assert::assertTrue(is_array($this->parser->toArray()), $message); + Assert::assertTrue(is_array($this->http_response->toArray()), $message); return $this; } @@ -122,7 +122,7 @@ public function assertArray(string $message = ''): Response */ public function assertContentType(string $content_type, string $message = ''): Response { - $type = $this->parser->getContentType(); + $type = $this->http_response->getContentType(); Assert::assertEquals( $content_type, @@ -198,7 +198,7 @@ public function assertContentTypeXml(string $message = ''): Response */ public function assertStatus(int $code, string $message = ''): Response { - Assert::assertEquals($this->parser->getCode(), $code, $message); + Assert::assertEquals($this->http_response->getCode(), $code, $message); return $this; } @@ -210,7 +210,7 @@ public function assertStatus(int $code, string $message = ''): Response */ public function assertKeyExists(string $key, string $message = ''): Response { - $data = $this->parser->toArray(); + $data = $this->http_response->toArray(); Assert::assertTrue(isset($data[$key]), $message); @@ -279,8 +279,8 @@ public function toArray(): array|object */ public function __call(string $method, array $params = []) { - if (method_exists($this->parser, $method)) { - return call_user_func([$this->parser, $method]); + if (method_exists($this->http_response, $method)) { + return call_user_func([$this->http_response, $method]); } throw new InvalidArgumentException( diff --git a/src/Testing/TestCase.php b/src/Testing/TestCase.php index d4d8c415..4727c083 100644 --- a/src/Testing/TestCase.php +++ b/src/Testing/TestCase.php @@ -31,20 +31,17 @@ class TestCase extends PHPUnitTestCase private array $headers = []; /** - * Format url + * Get the base url * - * @param $url * @return string */ - private function formatUrl(string $url): string + private function getBaseUrl(): string { - if (!$this->url) { - $this->url = app_env('APP_URL', 'http://127.0.0.1:5000'); + if (is_null($this->url)) { + return rtrim(app_env('APP_URL', 'http://127.0.0.1:5000')); } - $url = rtrim($this->url, '/') . $url; - - return trim($url, '/'); + return $this->url ?? 'http://127.0.0.1:5000'; } /** @@ -61,18 +58,31 @@ public function attach(array $attach): TestCase } /** - * Specify the additionnal who are use in the request + * Specify the additionnal headers * * @param array $headers * @return TestCase */ - public function withHeader(array $headers): TestCase + public function withHeaders(array $headers): TestCase { $this->headers = $headers; return $this; } + /** + * Specify the additionnal header + * + * @param array $headers + * @return TestCase + */ + public function withHeader(string $key, string $value): TestCase + { + $this->headers[$key] = $value; + + return $this; + } + /** * Get request * @@ -82,7 +92,7 @@ public function withHeader(array $headers): TestCase */ public function get(string $url, array $param = []): Response { - $http = new HttpClient($this->formatUrl($url)); + $http = new HttpClient($this->getBaseUrl()); $http->addHeaders($this->headers); @@ -98,7 +108,7 @@ public function get(string $url, array $param = []): Response */ public function post(string $url, array $param = []): Response { - $http = new HttpClient($this->formatUrl($url)); + $http = new HttpClient($this->getBaseUrl()); if (!empty($this->attach)) { $http->addAttach($this->attach); @@ -118,7 +128,7 @@ public function post(string $url, array $param = []): Response */ public function put(string $url, array $param = []): Response { - $http = new HttpClient($this->formatUrl($url)); + $http = new HttpClient($this->getBaseUrl()); $http->addHeaders($this->headers); diff --git a/src/Translate/README.md b/src/Translate/README.md index ab5824bf..23f3d1f1 100644 --- a/src/Translate/README.md +++ b/src/Translate/README.md @@ -1,8 +1,8 @@ # Bow Translate -Bow Framework's translate system is a very simple translate api. He support plurialize. +Bow Framework's translation system is a very simple translation API. Also supports pluralization. -This the sample configuration +This is the sample configuration ```php // frontend/lang/en/messages.php @@ -11,14 +11,12 @@ return [ ]; ``` -Let's show a little exemple: +Let's show a little example: ```php use Bow\Translate\Translator; echo Translator::translate('messages.welcome'); - // Or - -echo trans('messages.welcome'); +echo app_trans('messages.welcome'); ``` diff --git a/src/Translate/Translator.php b/src/Translate/Translator.php index bdc960d1..dc98ffd8 100644 --- a/src/Translate/Translator.php +++ b/src/Translate/Translator.php @@ -4,6 +4,7 @@ namespace Bow\Translate; +use Iterator; use Bow\Support\Arraydotify; class Translator @@ -188,7 +189,10 @@ public static function plurial(string $key, array $data = []): string private static function format(string $str, array $values = []): string { foreach ($values as $key => $value) { - $str = preg_replace('/{\s*' . $key . '\s*\}/', $value, $str); + if (is_array($value) || is_object($value) || $value instanceof Iterator) { + $value = json_encode($value); + } + $str = preg_replace('/{\s*' . $key . '\s*\}/', (string) $value, $str); } return $str; diff --git a/src/Translate/TranslatorConfiguration.php b/src/Translate/TranslatorConfiguration.php index c198928d..8130d656 100644 --- a/src/Translate/TranslatorConfiguration.php +++ b/src/Translate/TranslatorConfiguration.php @@ -15,15 +15,20 @@ class TranslatorConfiguration extends Configuration public function create(Loader $config): void { $this->container->bind('translate', function () use ($config) { - $auto_detected = is_null($config['translate.auto_detected']) - ? false - : $config['translate.auto_detected']; + $auto_detected = $config['translate.auto_detected'] ?? false; + $lang = $config['translate.lang']; + $dictionary = $config['translate.dictionary']; - return Translator::configure( - $config['translate.lang'], - $config['translate.dictionary'], - $auto_detected - ); + if ($auto_detected) { + $lang = app("request")->lang(); + if (is_string($lang)) { + $lang = strtolower($lang); + } else { + $lang = $config['translate.lang']; + } + } + + return Translator::configure($lang, $dictionary); }); } diff --git a/src/Validation/Exception/ValidationException.php b/src/Validation/Exception/ValidationException.php index 99c335ad..af43179e 100644 --- a/src/Validation/Exception/ValidationException.php +++ b/src/Validation/Exception/ValidationException.php @@ -19,13 +19,13 @@ class ValidationException extends HttpException * ValidationException constructor * * @param string $message - * @param string $status * @param array $errors + * @param string $status */ public function __construct( string $message, - string $status = 'VALIDATION_ERROR', - array $errors = [] + array $errors = [], + string $status = 'VALIDATION_ERROR' ) { parent::__construct($message, 400); $this->errors = $errors; diff --git a/src/Validation/FieldLexical.php b/src/Validation/FieldLexical.php index 223cddf2..24537ae8 100644 --- a/src/Validation/FieldLexical.php +++ b/src/Validation/FieldLexical.php @@ -4,54 +4,80 @@ namespace Bow\Validation; +use Iterator; + trait FieldLexical { /** * Get error debugging information * * @param string $key - * @param string|array $attributes + * @param string|array|int|float $value * @return ?string */ - private function lexical(string $key, string|array $attributes): ?string + private function lexical(string $key, string|array|int|float $value): ?string { - if (is_string($attributes) && isset($this->messages[$attributes])) { - return $this->messages[$attributes][$key] ?? $this->messages[$attributes]; - } + $data = array_merge( + $this->inputs ?? [], + is_array($value) ? $value : ['attribute' => $value] + ); + + if (is_array($value) && isset($value['attribute'])) { + $message = $this->messages[$value['attribute']][$key] ?? $this->messages[$value['attribute']] ?? null; + + if (is_string($message)) { + return $this->parseAttribute($data, $message); + } - if ( - is_array($attributes) - && isset($attributes['attribute']) - && isset($this->messages[$attributes['attribute']]) - ) { - return $this->messages[$attributes['attribute']][$key] ?? $this->messages[$attributes['attribute']]; + if (is_null($message)) { + return $this->parseFromTranslate($key, $data); + } } - if (is_string($attributes)) { - $attributes = ['attribute' => $attributes]; + if (is_string($value) && isset($this->messages[$value])) { + $message = $this->messages[$value][$key] ?? $this->messages[$value]; + + if (is_string($message)) { + return $this->parseAttribute($data, $message); + } } + return $this->parseFromTranslate($key, $data); + } + + /** + * Parse the translate content + * + * @param string $key + * @param array $data + * @return string + */ + private function parseFromTranslate(string $key, array $data) + { // Get lexical provided by dev app - $lexical = trans('validation.' . $key, $attributes); + $message = app_trans('validation.' . $key, $data); - if (is_null($lexical)) { - $lexical = $this->parseAttribute($attributes, $this->lexical[$key]); + if (is_null($message)) { + $message = $this->lexical[$key]; } - return $lexical; + return $this->parseAttribute($data, $message); } /** * Normalize beneficiaries * - * @param array $attributes + * @param array $attribute * @param string $lexical * @return string */ - private function parseAttribute(array $attributes, string $lexical): ?string + private function parseAttribute(array $attribute, string $lexical): ?string { - foreach ($attributes as $key => $value) { - $lexical = str_replace('{' . $key . '}', $value, $lexical); + foreach ($attribute as $key => $value) { + if (is_array($value) || is_object($value) || $value instanceof Iterator) { + $value = json_encode($value); + } + $lexical = str_replace('{' . $key . '}', (string) $value, $lexical); } return $lexical; diff --git a/src/Validation/RequestValidation.php b/src/Validation/RequestValidation.php index b1e68b0c..c8c09351 100644 --- a/src/Validation/RequestValidation.php +++ b/src/Validation/RequestValidation.php @@ -4,8 +4,9 @@ namespace Bow\Validation; -use BadMethodCallException; use Bow\Http\Request; +use BadMethodCallException; +use Bow\Validation\Exception\ValidationException; use Bow\Validation\Exception\AuthorizationException; abstract class RequestValidation @@ -51,7 +52,7 @@ public function __construct() if ((count($keys) == 1 && $keys[0] === '*') || count($keys) == 0) { $this->data = $this->request->all(); } else { - $this->data = $this->request->excepts($keys); + $this->data = $this->request->only($keys); } $this->validate = Validator::make($this->data, $this->rules(), $this->messages()); @@ -109,10 +110,9 @@ protected function messages() /** * Send fails authorization * - * @param mixed $response * @throws AuthorizationException */ - private function sendFailAuthorization($response = null) + private function sendFailAuthorization() { throw new AuthorizationException( 'You do not have permission to make a request' @@ -225,7 +225,9 @@ public function __call($name, array $arguments) return call_user_func_array([$this->request, $name], $arguments); } - throw new BadMethodCallException('The method ' . $name . ' does not define.'); + throw new BadMethodCallException( + 'The method ' . $name . ' does not defined.' + ); } /** diff --git a/src/Validation/Rules/RegexRule.php b/src/Validation/Rules/RegexRule.php index 725f7c4c..b358c283 100644 --- a/src/Validation/Rules/RegexRule.php +++ b/src/Validation/Rules/RegexRule.php @@ -9,19 +9,19 @@ trait RegexRule /** * Compile Regex Rule * - * [regex] Check that the contents of the field with a regular expression + * Check that the contents of the field with a regular expression * * @param string $key - * @param string $masque + * @param string|int|float $masque * @return void */ - protected function compileRegex(string $key, string $masque): void + protected function compileRegex(string $key, string|int|float $masque): void { - if (!preg_match("/^regex:(.+)+$/", $masque, $match)) { + if (!preg_match("/^regex:(.+)+$/", (string) $masque, $match)) { return; } - $regex = '~^' . $match[1] . '$~'; + $regex = '~' . addcslashes($match[1], "~") . '~'; if (preg_match($regex, $this->inputs[$key])) { return; diff --git a/src/Validation/Rules/StringRule.php b/src/Validation/Rules/StringRule.php index f27fff47..f0339983 100644 --- a/src/Validation/Rules/StringRule.php +++ b/src/Validation/Rules/StringRule.php @@ -5,6 +5,7 @@ namespace Bow\Validation\Rules; use Bow\Support\Str; +use Bow\Validation\Exception\ValidationException; trait StringRule { @@ -19,6 +20,10 @@ protected function compileRequired(string $key, string $masque): void { $error = false; + if (!preg_match("/^required$/", (string) $masque, $match)) { + return; + } + if (!isset($this->inputs[$key])) { $error = true; } @@ -39,6 +44,57 @@ protected function compileRequired(string $key, string $masque): void } } + /** + * Compile Required Rule + * + * @param string $key + * @param string $masque + * @return void + */ + protected function compileRequiredIf(string $key, string $masque): void + { + if (!preg_match("/^required_if:(.+)+$/", (string) $masque, $match)) { + return; + } + + array_shift($match); + + if (count($match) < 1) { + throw new ValidationException("The required_if is malformed"); + } + + $error = false; + $exists = false; + $fields = explode(",", $match[0]); + + foreach ($fields as $key => $field) { + if ($key == 0) { + $exists = isset($this->inputs[$field]); + } else { + $exists = $exists && isset($this->inputs[$field]); + } + } + + if ($exists && !isset($this->inputs[$key])) { + $error = true; + } + + if (!$error && isset($this->inputs[$key]) && (is_null($this->inputs[$key]) || $this->inputs[$key] === '')) { + $error = true; + } + + if ($error) { + $this->last_message = $message = $this->lexical('required_if', $key); + + $this->errors[$key][] = [ + "masque" => $masque, + "message" => $message + ]; + + $this->fails = true; + } + } + /** * Compile Empty Rule * diff --git a/src/Validation/Validate.php b/src/Validation/Validate.php index 13391862..a50d7353 100644 --- a/src/Validation/Validate.php +++ b/src/Validation/Validate.php @@ -129,7 +129,6 @@ public function throwError(): void throw new ValidationException( "Error on data validation sent", - "VALIADTION_ERROR", $this->messages ); } diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index d40b6ec6..e83fcaf2 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -64,6 +64,7 @@ class Validator */ protected array $rules = [ 'Required', + "RequiredIf", 'Max', 'Min', 'Lower', diff --git a/src/Validation/stubs/lexical.php b/src/Validation/stubs/lexical.php index ae786e4c..b0fc4fb2 100644 --- a/src/Validation/stubs/lexical.php +++ b/src/Validation/stubs/lexical.php @@ -1,25 +1,25 @@ 'The field {attribute} must be an email.', - 'required' => 'The field {attribute} is required.', - 'empty' => 'The field {attribute} is missing in the fields to be validated.', - 'min' => 'The field {attribute} must be at least {length} characters long.', - 'max' => 'The field {attribute} must not exceed {length} characters.', - 'same' => 'The field {attribute} must be the same as {value}.', - 'number' => 'The field {attribute} must be a number.', - 'int' => 'The field {attribute} must be an integer.', - 'float' => 'The field {attribute} must be a decimal.', + 'email' => 'The {attribute} field must be an email.', + 'required' => 'The {attribute} field is required.', + 'empty' => 'The {attribute} field is missing in the fields to be validated.', + 'min' => 'The {attribute} field must be at least {length} characters long.', + 'max' => 'The {attribute} field must not exceed {length} characters.', + 'same' => 'The {attribute} field must be the same as {value}.', + 'number' => 'The {attribute} field must be a number.', + 'int' => 'The {attribute} field must be an integer.', + 'float' => 'The {attribute} field must be a decimal.', 'alphanum' => 'Only alphanumeric characters are allowed for field {attribute}.', - 'in' => 'The field {attribute} must be one of the following {value}.', - 'size' => 'The field {attribute} must be {length} characters long.', + 'in' => 'The {attribute} field must be one of the following {value}.', + 'size' => 'The {attribute} field must be {length} characters long.', 'lower' => 'Only lowercase letters are allowed for field {attribute}.', 'upper' => 'Only uppercase letters are allowed for field {attribute}.', 'alpha' => 'Only alphabetic characters are allowed for field {attribute}.', - 'exists' => 'The field {attribute} does not exists.', - 'not_exists' => 'The field {attribute} already exists.', - 'unique' => 'The field {attribute} must be unique.', - 'date' => 'The field {attribute} must use the format: yyyy-mm-dd', - 'datetime' => 'The field {attribute} must use the format: yyyy-mm-dd hh:mm:ss', - 'regex' => 'The field {attribute} does not match the pattern', + 'exists' => 'The {attribute} field does not exists.', + 'not_exists' => 'The {attribute} field already exists.', + 'unique' => 'The {attribute} field must be unique.', + 'date' => 'The {attribute} field must use the format: yyyy-mm-dd', + 'datetime' => 'The {attribute} field must use the format: yyyy-mm-dd hh:mm:ss', + 'regex' => 'The {attribute} field does not match the pattern', ]; diff --git a/src/View/View.php b/src/View/View.php index f28c0c91..3ad2a0b8 100644 --- a/src/View/View.php +++ b/src/View/View.php @@ -4,9 +4,10 @@ namespace Bow\View; +use Tintin\Tintin; use BadMethodCallException; -use Bow\Contracts\ResponseInterface; use Bow\View\EngineAbstract; +use Bow\Contracts\ResponseInterface; use Bow\View\Exception\ViewException; class View implements ResponseInterface diff --git a/tests/Application/ApplicationTest.php b/tests/Application/ApplicationTest.php index d1605e22..1f88526b 100644 --- a/tests/Application/ApplicationTest.php +++ b/tests/Application/ApplicationTest.php @@ -29,6 +29,7 @@ public function test_instance_of_application() $request = Mockery::mock(Request::class); $request->allows()->method()->andReturns("GET"); + $request->allows()->capture()->andReturns(null); $request->allows()->get("_method")->andReturns(""); $app = Application::make($request, $response); @@ -45,6 +46,7 @@ public function test_one_time_application_boot() $request = Mockery::mock(Request::class); $request->allows()->method()->andReturns("GET"); + $request->allows()->capture()->andReturns(null); $request->allows()->get("_method")->andReturns(""); $app = Application::make($request, $response); @@ -57,16 +59,18 @@ public function test_one_time_application_boot() public function test_send_application_with_404_status() { + $this->expectException(RouterException::class); + $response = Mockery::mock(Response::class); $request = Mockery::mock(Request::class); // Response mock method $response->allows()->addHeader('X-Powered-By', 'Bow Framework'); $response->allows()->status(404); - $response->allows()->send('Cannot GET / 404'); // Request mock method $request->allows()->method()->andReturns("GET"); + $request->allows()->capture()->andReturns(null); $request->allows()->path()->andReturns("/"); $request->allows()->get("_method")->andReturns(""); @@ -80,8 +84,7 @@ public function test_send_application_with_404_status() $app = new Application($request, $response); $app->bind($config); - - $this->assertFalse($app->send()); + $app->send(); } public function test_send_application_with_matched_route() @@ -96,6 +99,7 @@ public function test_send_application_with_matched_route() // Request mock method $request->allows()->method()->andReturns("GET"); + $request->allows()->capture()->andReturns(null); $request->allows()->path()->andReturns("/"); $request->allows()->get("_method")->andReturns(""); @@ -128,6 +132,7 @@ public function test_send_application_with_no_matched_route() // Request mock method $request->allows()->method()->andReturns("GET"); + $request->allows()->capture()->andReturns(null); $request->allows()->path()->andReturns("/name"); $request->allows()->get("_method")->andReturns(""); diff --git a/tests/Auth/AuthenticationTest.php b/tests/Auth/AuthenticationTest.php index daf56167..e233abaa 100644 --- a/tests/Auth/AuthenticationTest.php +++ b/tests/Auth/AuthenticationTest.php @@ -44,6 +44,7 @@ public function test_it_should_be_a_default_guard() { $config = TestingConfiguration::getConfig(); $auth = Auth::getInstance(); + $this->assertEquals($auth->getName(), $config["auth"]["default"]); $this->assertEquals($auth->getName(), "web"); } @@ -104,7 +105,7 @@ public function test_attempt_login_with_jwt_provider() $this->assertInstanceOf(Authentication::class, $user); $this->assertTrue($auth->check()); $this->assertEquals($auth->id(), $user->id); - $this->assertRegExp("/^([a-zA-Z0-9_-]+\.){2}[a-zA-Z0-9_-]+$/", $token); + $this->assertMatchesRegularExpression("/^([a-zA-Z0-9_-]+\.){2}[a-zA-Z0-9_-]+$/", $token); } public function test_direct_login_with_jwt_provider() @@ -118,7 +119,7 @@ public function test_direct_login_with_jwt_provider() $this->assertTrue($auth->check()); $this->assertInstanceOf(Authentication::class, $user); $this->assertEquals($auth->id(), $user->id); - $this->assertRegExp("/^([a-zA-Z0-9_-]+\.){2}[a-zA-Z0-9_-]+$/", $token); + $this->assertMatchesRegularExpression("/^([a-zA-Z0-9_-]+\.){2}[a-zA-Z0-9_-]+$/", $token); } public function test_attempt_login_with_jwt_provider_fail() @@ -137,6 +138,7 @@ public function test_attempt_login_with_jwt_provider_fail() public function test_attempt_login_with_session_provider() { $this->expectException(AuthenticationException::class); + $auth = Auth::guard('web'); $auth->attempts([ "username" => "papac", diff --git a/tests/Cache/CacheDatabaseTest.php b/tests/Cache/CacheDatabaseTest.php index 49db5514..179a1729 100644 --- a/tests/Cache/CacheDatabaseTest.php +++ b/tests/Cache/CacheDatabaseTest.php @@ -22,8 +22,8 @@ public static function setUpBeforeClass(): void `expire` datetime null )"); - Cache::confirgure($config["cache"]); - Cache::cache("database"); + Cache::configure($config["cache"]); + Cache::store("database"); } public function test_create_cache() @@ -38,7 +38,7 @@ public function test_get_cache() $this->assertEquals(Cache::get('name'), 'Dakia'); } - public function test_AddWithCallbackCache() + public function test_add_with_callback_cache() { $result = Cache::add('lastname', fn () => 'Franck'); $result = $result && Cache::add('age', fn () => 25, 20000); @@ -46,14 +46,14 @@ public function test_AddWithCallbackCache() $this->assertEquals($result, true); } - public function test_GetCallbackCache() + public function test_get_callback_cache() { $this->assertEquals(Cache::get('lastname'), 'Franck'); $this->assertEquals(Cache::get('age'), 25); } - public function test_AddArrayCache() + public function test_add_array_cache() { $result = Cache::add('address', [ 'tel' => "49929598", @@ -64,7 +64,7 @@ public function test_AddArrayCache() $this->assertEquals($result, true); } - public function test_GetArrayCache() + public function test_get_array_cache() { $result = Cache::get('address'); diff --git a/tests/Cache/CacheFilesystemTest.php b/tests/Cache/CacheFilesystemTest.php index 5f6ea961..d957b2ee 100644 --- a/tests/Cache/CacheFilesystemTest.php +++ b/tests/Cache/CacheFilesystemTest.php @@ -11,8 +11,8 @@ protected function setUp(): void { parent::setUp(); $config = TestingConfiguration::getConfig(); - Cache::confirgure($config["cache"]); - Cache::cache("file"); + Cache::configure($config["cache"]); + Cache::store("file"); } public function test_create_cache() @@ -27,7 +27,7 @@ public function test_get_cache() $this->assertEquals(Cache::get('name'), 'Dakia'); } - public function test_AddWithCallbackCache() + public function test_add_with_callback_cache() { $result = Cache::add('lastname', fn () => 'Franck'); $result = $result && Cache::add('age', fn () => 25, 20000); @@ -35,14 +35,14 @@ public function test_AddWithCallbackCache() $this->assertEquals($result, true); } - public function test_GetCallbackCache() + public function test_get_callback_cache() { $this->assertEquals(Cache::get('lastname'), 'Franck'); $this->assertEquals(Cache::get('age'), 25); } - public function test_AddArrayCache() + public function test_add_array_cache() { $result = Cache::add('address', [ 'tel' => "49929598", @@ -53,7 +53,7 @@ public function test_AddArrayCache() $this->assertEquals($result, true); } - public function test_GetArrayCache() + public function test_get_array_cache() { $result = Cache::get('address'); @@ -94,7 +94,7 @@ public function test_time_of_empty() { $result = Cache::timeOf('lastname'); - $this->assertEquals('+', $result); + $this->assertTrue(is_numeric($result)); } public function test_time_of_empty_2() diff --git a/tests/Cache/CacheRedisTest.php b/tests/Cache/CacheRedisTest.php index dec3f5ea..d24d8aae 100644 --- a/tests/Cache/CacheRedisTest.php +++ b/tests/Cache/CacheRedisTest.php @@ -11,8 +11,8 @@ protected function setUp(): void { parent::setUp(); $config = TestingConfiguration::getConfig(); - Cache::confirgure($config["cache"]); - Cache::cache("redis"); + Cache::configure($config["cache"]); + Cache::store("redis"); } public function test_create_cache() @@ -27,7 +27,7 @@ public function test_get_cache() $this->assertEquals(Cache::get('name'), 'Dakia'); } - public function test_AddWithCallbackCache() + public function test_add_with_callback_cache() { $result = Cache::add('lastname', fn () => 'Franck'); $result = $result && Cache::add('age', fn () => 25, 20000); @@ -35,14 +35,14 @@ public function test_AddWithCallbackCache() $this->assertEquals($result, true); } - public function test_GetCallbackCache() + public function test_get_callback_cache() { $this->assertEquals(Cache::get('lastname'), 'Franck'); $this->assertEquals(Cache::get('age'), 25); } - public function test_AddArrayCache() + public function test_add_array_cache() { $result = Cache::add('address', [ 'tel' => "49929598", @@ -53,7 +53,7 @@ public function test_AddArrayCache() $this->assertEquals($result, true); } - public function test_GetArrayCache() + public function test_get_array_cache() { $result = Cache::get('address'); diff --git a/tests/Config/TestingConfiguration.php b/tests/Config/TestingConfiguration.php index 63009e33..d542fb42 100644 --- a/tests/Config/TestingConfiguration.php +++ b/tests/Config/TestingConfiguration.php @@ -15,6 +15,39 @@ public function __construct() is_dir(TESTING_RESOURCE_BASE_DIRECTORY) || mkdir(TESTING_RESOURCE_BASE_DIRECTORY, 0777); } + /** + * Configure the testing + * + * @param array $configurations + * @return void + */ + public static function withConfigurations(array $configurations) + { + KernelTesting::withConfigurations($configurations); + } + + /** + * Configure the testing + * + * @param array $middlewares + * @return void + */ + public static function withMiddlewares(array $middlewares) + { + KernelTesting::withMiddlewares($middlewares); + } + + /** + * Set the loading events + * + * @param array $events + * @return void + */ + public static function withEvents(array $events): void + { + KernelTesting::withEvents($events); + } + /** * Get the configuration for testing * diff --git a/tests/Config/stubs/app.php b/tests/Config/stubs/app.php index 023def4b..aadfc824 100644 --- a/tests/Config/stubs/app.php +++ b/tests/Config/stubs/app.php @@ -2,5 +2,6 @@ return [ "root" => "", - "auto_csrf" => false + "auto_csrf" => false, + "env_file" => realpath(__DIR__ . "/../../Support/stubs/env.json"), ]; diff --git a/tests/Config/stubs/cache.php b/tests/Config/stubs/cache.php index 71770976..69e79c81 100644 --- a/tests/Config/stubs/cache.php +++ b/tests/Config/stubs/cache.php @@ -15,25 +15,11 @@ "table" => "caches", ], + // The redis connection "redis" => [ 'driver' => 'redis', - 'host' => app_env('REDIS_HOSTNAME', '127.0.0.1'), - 'port' => app_env('REDIS_PORT', 6379), - 'timeout' => 2.5, - 'ssl' => false, - 'username' => app_env('REDIS_USERNAME'), - 'password' => app_env('REDIS_PASSWORD'), - 'database' => app_env('REDIS_CACHE_DB', '1'), + 'database' => app_env('REDIS_CACHE_DB', 5), "prefix" => "__app__", - 'slave' => [ - 'host' => app_env('REDIS_HOSTNAME', '127.0.0.1'), - 'port' => app_env('REDIS_PORT', 6379), - 'timeout' => 2.5, - 'ssl' => false, - 'username' => app_env('REDIS_USERNAME'), - 'password' => app_env('REDIS_PASSWORD'), - 'database' => app_env('REDIS_CACHE_DB', '1'), - ] ] ] ]; diff --git a/tests/Config/stubs/database.php b/tests/Config/stubs/database.php index 7225b4fb..f6cb2527 100644 --- a/tests/Config/stubs/database.php +++ b/tests/Config/stubs/database.php @@ -31,5 +31,26 @@ 'database' => ':memory:', 'prefix' => '' ] + ], + + "redis" => [ + 'driver' => 'redis', + 'host' => app_env('REDIS_HOSTNAME', '127.0.0.1'), + 'port' => app_env('REDIS_PORT', 6379), + 'timeout' => 2.5, + 'ssl' => false, + 'username' => app_env('REDIS_USERNAME'), + 'password' => app_env('REDIS_PASSWORD'), + 'database' => app_env('REDIS_CACHE_DB', '1'), + "prefix" => "__app__", + 'slave' => [ + 'host' => app_env('REDIS_HOSTNAME', '127.0.0.1'), + 'port' => app_env('REDIS_PORT', 6379), + 'timeout' => 2.5, + 'ssl' => false, + 'username' => app_env('REDIS_USERNAME'), + 'password' => app_env('REDIS_PASSWORD'), + 'database' => app_env('REDIS_CACHE_DB', '1'), + ] ] ]; diff --git a/tests/Config/stubs/policier.php b/tests/Config/stubs/policier.php index 33e276dc..ded1be3c 100644 --- a/tests/Config/stubs/policier.php +++ b/tests/Config/stubs/policier.php @@ -46,7 +46,7 @@ /** * Hashing algorithm being used * - * HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, + * HS256, HS384, HS512, ES256, ES384, ES512, */ - "alg" => "HS256", + "alg" => "HS512", ]; diff --git a/tests/Config/stubs/queue.php b/tests/Config/stubs/queue.php index 241290e1..85f39dbb 100644 --- a/tests/Config/stubs/queue.php +++ b/tests/Config/stubs/queue.php @@ -4,7 +4,7 @@ /** * The defaut connexion */ - "default" => "beanstalkd", + "default" => "sync", /** * The queue drive connection @@ -21,8 +21,8 @@ * The beanstalkd connexion */ "beanstalkd" => [ - "hostname" => "127.0.0.0", - "port" => 11301, + "hostname" => "127.0.0.1", + "port" => 11300, "timeout" => 10, ], @@ -30,9 +30,21 @@ * The sqs connexion */ "sqs" => [ - "hostname" => "127.0.0.0", - "port" => 11300, - "timeout" => 10, + 'profile' => 'default', + 'region' => 'ap-south-1', + 'version' => 'latest', + 'url' => getenv("AWS_SQS_URL"), + 'credentials' => [ + 'key' => getenv('AWS_KEY'), + 'secret' => getenv('AWS_SECRET'), + ], + ], + + /** + * The sqs connexion + */ + "database" => [ + 'table' => "queues", ] ] ]; diff --git a/tests/Config/stubs/storage.php b/tests/Config/stubs/storage.php index 3d9aa136..81857865 100644 --- a/tests/Config/stubs/storage.php +++ b/tests/Config/stubs/storage.php @@ -27,7 +27,7 @@ 'password' => app_env('FTP_PASSWORD', 'password'), 'username' => app_env('FTP_USERNAME', 'username'), 'port' => app_env('FTP_PORT', 21), - 'root' => app_env('FTP_ROOT', sys_get_temp_dir()), // Start directory + 'root' => app_env('FTP_ROOT', '/tmp'), // Start directory 'tls' => app_env('FTP_SSL', false), // `true` enable the secure connexion. 'timeout' => app_env('FTP_TIMEOUT', 90) // Temps d'attente de connection ], @@ -38,10 +38,10 @@ 's3' => [ "driver" => "s3", 'credentials' => [ - 'key' => app_env('AWS_S3_KEY'), - 'secret' => app_env('AWS_S3_SECRET'), + 'key' => getenv('AWS_KEY'), + 'secret' => getenv('AWS_SECRET'), ], - 'bucket' => app_env('AWS_S3_BUCKET'), + 'bucket' => getenv('AWS_S3_BUCKET'), 'region' => 'us-east-1', 'version' => 'latest' ] diff --git a/tests/Config/stubs/translate.php b/tests/Config/stubs/translate.php new file mode 100644 index 00000000..38c282b8 --- /dev/null +++ b/tests/Config/stubs/translate.php @@ -0,0 +1,19 @@ + 'fr', + + /** + * When the value is true, the translation system + * will detect the language of the client and will translate according to + */ + 'auto_detected' => false, + + /** + * Path to the language repeater + */ + 'dictionary' => __DIR__ . '/../../Translate/stubs', +]; diff --git a/tests/Console/GeneratorBasicTest.php b/tests/Console/GeneratorBasicTest.php index d8597e86..78640aff 100644 --- a/tests/Console/GeneratorBasicTest.php +++ b/tests/Console/GeneratorBasicTest.php @@ -16,8 +16,8 @@ public function test_generate_stubs() ]); $this->assertNotNull($content); - $this->assertRegExp("@\nnamespace\sGenerator\\\Testing;\n@", $content); - $this->assertRegExp("@\nclass\sCreateUserCommand\sextends\sConsoleCommand\n@", $content); + $this->assertMatchesRegularExpression("@\nnamespace\sGenerator\\\Testing;\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sCreateUserCommand\sextends\sConsoleCommand\n@", $content); } public function test_generate_stub_without_data() @@ -26,8 +26,8 @@ public function test_generate_stub_without_data() $content = $generator->makeStubContent('command', []); $this->assertNotNull($content); - $this->assertRegExp("@\nnamespace\s\{baseNamespace\}\{namespace\};\n@", $content); - $this->assertRegExp("@\nclass\s\{className\}\sextends\sConsoleCommand\n@", $content); + $this->assertMatchesRegularExpression("@\nnamespace\s\{baseNamespace\}\{namespace\};\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\s\{className\}\sextends\sConsoleCommand\n@", $content); } public function test_generate_by_writing_file() diff --git a/tests/Console/GeneratorDeepTest.php b/tests/Console/GeneratorDeepTest.php index 17a9635a..a497ec2e 100644 --- a/tests/Console/GeneratorDeepTest.php +++ b/tests/Console/GeneratorDeepTest.php @@ -20,8 +20,8 @@ public function test_generate_command_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nnamespace\sApp\\\Commands;\n@", $content); - $this->assertRegExp("@\nclass\sFakeCommand\sextends\sConsoleCommand\n@", $content); + $this->assertMatchesRegularExpression("@\nnamespace\sApp\\\Commands;\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeCommand\sextends\sConsoleCommand\n@", $content); } public function test_generate_configuration_stubs() @@ -35,8 +35,8 @@ public function test_generate_configuration_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nnamespace\sApp\\\Configurations;\n@", $content); - $this->assertRegExp("@\nclass\sFakeConfiguration\sextends\sConfiguration\n@", $content); + $this->assertMatchesRegularExpression("@\nnamespace\sApp\\\Configurations;\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeConfiguration\sextends\sConfiguration\n@", $content); } public function test_generate_event_stubs() @@ -50,8 +50,8 @@ public function test_generate_event_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nnamespace\sApp\\\Events;\n@", $content); - $this->assertRegExp("@\nclass\sFakeEvent\simplements\sAppEvent\n@", $content); + $this->assertMatchesRegularExpression("@\nnamespace\sApp\\\Events;\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeEvent\simplements\sAppEvent\n@", $content); } public function test_generate_exception_stubs() @@ -65,8 +65,8 @@ public function test_generate_exception_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nnamespace\sApp\\\Exceptions;\n@", $content); - $this->assertRegExp("@\nclass\sFakeException\sextends\sException\n@", $content); + $this->assertMatchesRegularExpression("@\nnamespace\sApp\\\Exceptions;\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeException\sextends\sException\n@", $content); } public function test_generate_listener_stubs() @@ -80,8 +80,8 @@ public function test_generate_listener_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nnamespace\sApp\\\Listeners;\n@", $content); - $this->assertRegExp("@\nclass\sFakeListener\simplements\sEventListener\n@", $content); + $this->assertMatchesRegularExpression("@\nnamespace\sApp\\\Listeners;\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeListener\simplements\sEventListener\n@", $content); } public function test_generate_middleware_stubs() @@ -95,8 +95,8 @@ public function test_generate_middleware_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nnamespace\sApp\\\Middlewares;\n@", $content); - $this->assertRegExp("@\nclass\sFakeMiddleware\simplements\sBaseMiddleware\n@", $content); + $this->assertMatchesRegularExpression("@\nnamespace\sApp\\\Middlewares;\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeMiddleware\simplements\sBaseMiddleware\n@", $content); } public function test_generate_producer_stubs() @@ -110,8 +110,8 @@ public function test_generate_producer_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nnamespace\sApp\\\Producers;\n@", $content); - $this->assertRegExp("@\nclass\sFakeProducer\sextends\sProducerService\n@", $content); + $this->assertMatchesRegularExpression("@\nnamespace\sApp\\\Producers;\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeProducer\sextends\sProducerService\n@", $content); } public function test_generate_seeder_stubs() @@ -137,8 +137,8 @@ public function test_generate_service_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nnamespace\sApp\\\Services;\n@", $content); - $this->assertRegExp("@\nclass\sFakeService\n@", $content); + $this->assertMatchesRegularExpression("@\nnamespace\sApp\\\Services;\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeService\n@", $content); } public function test_generate_validation_stubs() @@ -152,8 +152,8 @@ public function test_generate_validation_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nnamespace\sApp\\\Validations;\n@", $content); - $this->assertRegExp("@\nclass\sFakeValidationRequest\sextends\sRequestValidation\n@", $content); + $this->assertMatchesRegularExpression("@\nnamespace\sApp\\\Validations;\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeValidationRequest\sextends\sRequestValidation\n@", $content); } public function test_generate_cache_migration_stubs() @@ -165,7 +165,7 @@ public function test_generate_cache_migration_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nclass\sFakeCacheMigration\sextends\sMigration\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeCacheMigration\sextends\sMigration\n@", $content); } public function test_generate_session_migration_stubs() @@ -177,7 +177,21 @@ public function test_generate_session_migration_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nclass\sFakeSessionMigration\sextends\sMigration\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeSessionMigration\sextends\sMigration\n@", $content); + } + + public function test_generate_queue_migration_stubs() + { + $generator = new Generator(TESTING_RESOURCE_BASE_DIRECTORY, 'QueueTableMigration'); + $content = $generator->makeStubContent('model/queue', [ + "className" => "QueueTableMigration", + ]); + + $this->assertNotNull($content); + $this->assertMatchesSnapshot($content); + $this->assertMatchesRegularExpression("@\nclass\sQueueTableMigration\sextends\sMigration\n@", $content); + $this->assertStringContainsString("\$this->create(\"queues\", function (SQLGenerator \$table) {", $content); + $this->assertStringContainsString("\$table->addInteger('attempts', [\"default\" => 3]);\n", $content); } public function test_generate_table_migration_stubs() @@ -190,7 +204,7 @@ public function test_generate_table_migration_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nclass\sFakeTableMigration\sextends\sMigration\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeTableMigration\sextends\sMigration\n@", $content); } public function test_generate_create_migration_stubs() @@ -203,7 +217,7 @@ public function test_generate_create_migration_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nclass\sFakeCreateTableMigration\sextends\sMigration\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeCreateTableMigration\sextends\sMigration\n@", $content); } public function test_generate_standard_migration_stubs() @@ -216,7 +230,7 @@ public function test_generate_standard_migration_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nclass\sFakeStandardTableMigration\sextends\sMigration\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sFakeStandardTableMigration\sextends\sMigration\n@", $content); } public function test_generate_model_stubs() @@ -231,7 +245,7 @@ public function test_generate_model_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nclass\sExample\sextends\sModel\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sExample\sextends\sModel\n@", $content); } public function test_generate_controller_stubs() @@ -245,7 +259,7 @@ public function test_generate_controller_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp("@\nclass\sExampleController\sextends\sController\n@", $content); + $this->assertMatchesRegularExpression("@\nclass\sExampleController\sextends\sController\n@", $content); } public function test_generate_controller_no_plain_stubs() @@ -259,14 +273,14 @@ public function test_generate_controller_no_plain_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp('@\nclass\sExampleController\sextends\sController\n@', $content); - $this->assertRegExp('@public\sfunction\sindex()@', $content); - $this->assertRegExp('@public\sfunction\screate()@', $content); - $this->assertRegExp('@public\sfunction\supdate\(Request\s\$request,\smixed\s\$id\)@', $content); - $this->assertRegExp('@public\sfunction\sshow\(mixed\s\$id\)@', $content); - $this->assertRegExp('@public\sfunction\sedit\(mixed\s\$id\)@', $content); - $this->assertRegExp('@public\sfunction\sstore\(Request\s\$request\)@', $content); - $this->assertRegExp('@public\sfunction\sdestroy\(mixed\s\$id\)@', $content); + $this->assertMatchesRegularExpression('@\nclass\sExampleController\sextends\sController\n@', $content); + $this->assertMatchesRegularExpression('@public\sfunction\sindex()@', $content); + $this->assertMatchesRegularExpression('@public\sfunction\screate()@', $content); + $this->assertMatchesRegularExpression('@public\sfunction\supdate\(Request\s\$request,\smixed\s\$id\)@', $content); + $this->assertMatchesRegularExpression('@public\sfunction\sshow\(mixed\s\$id\)@', $content); + $this->assertMatchesRegularExpression('@public\sfunction\sedit\(mixed\s\$id\)@', $content); + $this->assertMatchesRegularExpression('@public\sfunction\sstore\(Request\s\$request\)@', $content); + $this->assertMatchesRegularExpression('@public\sfunction\sdestroy\(mixed\s\$id\)@', $content); } public function test_generate_controller_rest_stubs() @@ -280,11 +294,11 @@ public function test_generate_controller_rest_stubs() $this->assertNotNull($content); $this->assertMatchesSnapshot($content); - $this->assertRegExp('@\nclass\sExampleController\sextends\sController\n@', $content); - $this->assertRegExp('@public\sfunction\sindex()@', $content); - $this->assertRegExp('@public\sfunction\supdate\(Request\s\$request,\smixed\s\$id\)@', $content); - $this->assertRegExp('@public\sfunction\sshow\(Request\s\$request,\smixed\s\$id\)@', $content); - $this->assertRegExp('@public\sfunction\sstore\(Request\s\$request\)@', $content); - $this->assertRegExp('@public\sfunction\sdestroy\(Request\s\$request,\smixed\s\$id\)@', $content); + $this->assertMatchesRegularExpression('@\nclass\sExampleController\sextends\sController\n@', $content); + $this->assertMatchesRegularExpression('@public\sfunction\sindex()@', $content); + $this->assertMatchesRegularExpression('@public\sfunction\supdate\(Request\s\$request,\smixed\s\$id\)@', $content); + $this->assertMatchesRegularExpression('@public\sfunction\sshow\(Request\s\$request,\smixed\s\$id\)@', $content); + $this->assertMatchesRegularExpression('@public\sfunction\sstore\(Request\s\$request\)@', $content); + $this->assertMatchesRegularExpression('@public\sfunction\sdestroy\(Request\s\$request,\smixed\s\$id\)@', $content); } } diff --git a/tests/Console/__snapshots__/GeneratorDeepTest__test_generate_event_stubs__1.txt b/tests/Console/__snapshots__/GeneratorDeepTest__test_generate_event_stubs__1.txt index 425396f9..7accf36b 100644 --- a/tests/Console/__snapshots__/GeneratorDeepTest__test_generate_event_stubs__1.txt +++ b/tests/Console/__snapshots__/GeneratorDeepTest__test_generate_event_stubs__1.txt @@ -4,11 +4,11 @@ namespace App\Events; use Bow\Event\Contracts\AppEvent; use Bow\Event\Dispatchable; -use Bow\Queue\Traits\SerializesModels; +use Bow\Support\Serializes; class FakeEvent implements AppEvent { - use Dispatchable, SerializesModels; + use Dispatchable, Serializes; /** * Create a new event instance. diff --git a/tests/Console/__snapshots__/GeneratorDeepTest__test_generate_middleware_stubs__1.txt b/tests/Console/__snapshots__/GeneratorDeepTest__test_generate_middleware_stubs__1.txt index f98bc4d8..1cb723fa 100644 --- a/tests/Console/__snapshots__/GeneratorDeepTest__test_generate_middleware_stubs__1.txt +++ b/tests/Console/__snapshots__/GeneratorDeepTest__test_generate_middleware_stubs__1.txt @@ -15,7 +15,7 @@ class FakeMiddleware implements BaseMiddleware * @param array $args * @return mixed */ - public function process(Request $request, callable $next, array $args = []): void + public function process(Request $request, callable $next, array $args = []): mixed { // Code Here diff --git a/tests/Console/__snapshots__/GeneratorDeepTest__test_generate_queue_migration_stubs__1.txt b/tests/Console/__snapshots__/GeneratorDeepTest__test_generate_queue_migration_stubs__1.txt new file mode 100644 index 00000000..0c85a498 --- /dev/null +++ b/tests/Console/__snapshots__/GeneratorDeepTest__test_generate_queue_migration_stubs__1.txt @@ -0,0 +1,38 @@ +create("queues", function (SQLGenerator $table) { + $table->addString('id', ["primary" => true]); + $table->addString('queue'); + $table->addText('payload'); + $table->addInteger('attempts', ["default" => 3]); + $table->addEnum('status', [ + "size" => ["waiting", "processing", "reserved", "failed", "done"], + "default" => "waiting", + ]); + $table->addDatetime('avalaibled_at'); + $table->addDatetime('reserved_at', ["nullable" => true, "default" => null]); + $table->addDatetime('created_at'); + }); + } + + /** + * Rollback migration + */ + public function rollback(): void + { + $this->dropIfExists("queues"); + if ($this->adapter->getName() === 'pgsql') { + $this->addSql("DROP TYPE IF EXISTS queue_status"); + } + } +} diff --git a/tests/Database/Migration/Pgsql/SQLGeneratorTest.php b/tests/Database/Migration/Pgsql/SQLGeneratorTest.php index 235debdd..a24ad598 100644 --- a/tests/Database/Migration/Pgsql/SQLGeneratorTest.php +++ b/tests/Database/Migration/Pgsql/SQLGeneratorTest.php @@ -26,19 +26,19 @@ public function test_add_column_sql_statement() { $this->generator->addColumn('name', 'int'); $sql = $this->generator->make(); - $this->assertEquals($sql, 'name INT NOT NULL'); + $this->assertEquals($sql, '"name" INT NOT NULL'); $this->generator->addColumn('name', 'string'); $sql = $this->generator->make(); - $this->assertNotEquals($sql, 'name STRING NOT NULL'); + $this->assertNotEquals($sql, '"name" STRING NOT NULL'); $this->generator->addColumn('name', 'string'); $sql = $this->generator->make(); - $this->assertEquals($sql, 'name VARCHAR(255) NOT NULL'); + $this->assertEquals($sql, '"name" VARCHAR(255) NOT NULL'); $this->generator->addColumn('name', 'string', ['default' => 'bow', 'size' => 100]); $sql = $this->generator->make(); - $this->assertEquals($sql, "name VARCHAR(100) NOT NULL DEFAULT 'bow'"); + $this->assertEquals($sql, '"name" VARCHAR(100) NOT NULL DEFAULT \'bow\''); } /** @@ -48,19 +48,19 @@ public function test_change_column_sql_statement() { $this->generator->changeColumn('name', 'int'); $sql = $this->generator->make(); - $this->assertEquals($sql, 'MODIFY COLUMN name INT NOT NULL'); + $this->assertEquals($sql, 'MODIFY COLUMN "name" INT NOT NULL'); $this->generator->changeColumn('name', 'string'); $sql = $this->generator->make(); - $this->assertNotEquals($sql, 'MODIFY COLUMN name STRING NOT NULL'); + $this->assertNotEquals($sql, 'MODIFY COLUMN "name" STRING NOT NULL'); $this->generator->changeColumn('name', 'string'); $sql = $this->generator->make(); - $this->assertEquals($sql, 'MODIFY COLUMN name VARCHAR(255) NOT NULL'); + $this->assertEquals($sql, 'MODIFY COLUMN "name" VARCHAR(255) NOT NULL'); $this->generator->changeColumn('name', 'string', ['default' => 'bow', 'size' => 100]); $sql = $this->generator->make(); - $this->assertEquals($sql, "MODIFY COLUMN name VARCHAR(100) NOT NULL DEFAULT 'bow'"); + $this->assertEquals($sql, 'MODIFY COLUMN "name" VARCHAR(100) NOT NULL DEFAULT \'bow\''); } /** @@ -70,7 +70,7 @@ public function test_rename_column_sql_statement_for_mysql() { $this->generator->renameColumn('name', 'fullname'); $sql = $this->generator->make(); - $this->assertEquals($sql, 'RENAME COLUMN name TO fullname'); + $this->assertEquals($sql, 'RENAME COLUMN "name" TO fullname'); } /** @@ -128,7 +128,7 @@ public function test_should_create_correct_datetime_sql_statement() $this->generator->addDatetime('created_at', ['default' => 'CURRENT_TIMESTAMP']); $sql = $this->generator->make(); - $this->assertEquals($sql, 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'); + $this->assertEquals($sql, '"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'); } public function test_should_create_not_correct_datetime_sql_statement() @@ -136,7 +136,7 @@ public function test_should_create_not_correct_datetime_sql_statement() $this->generator->addDatetime('created_at'); $sql = $this->generator->make(); - $this->assertNotEquals($sql, 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'); + $this->assertNotEquals($sql, '"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'); } public function test_should_create_correct_timestamps_sql_statement() @@ -144,6 +144,6 @@ public function test_should_create_correct_timestamps_sql_statement() $this->generator->addTimestamps(); $sql = $this->generator->make(); - $this->assertEquals($sql, 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'); + $this->assertEquals($sql, '"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'); } } diff --git a/tests/Database/Migration/Pgsql/SQLGenetorHelpersTest.php b/tests/Database/Migration/Pgsql/SQLGenetorHelpersTest.php index 6334c144..ab046d86 100644 --- a/tests/Database/Migration/Pgsql/SQLGenetorHelpersTest.php +++ b/tests/Database/Migration/Pgsql/SQLGenetorHelpersTest.php @@ -27,11 +27,11 @@ public function test_add_string_sql_statement(string $type, string $method, int| $type = strtoupper($type); $sql = $this->generator->{"add$method"}('name')->make(); - $this->assertNotEquals($sql, 'name STRING NOT NULL'); + $this->assertNotEquals($sql, '"name" STRING NOT NULL'); if (preg_match('/STRING|VARCHAR/', $type)) { - $this->assertEquals($sql, "name {$type}(255) NOT NULL"); + $this->assertEquals($sql, "\"name\" {$type}(255) NOT NULL"); } else { - $this->assertEquals($sql, "name {$type} NOT NULL"); + $this->assertEquals($sql, "\"name\" {$type} NOT NULL"); } if (preg_match('/TEXT/', $type)) { @@ -40,20 +40,20 @@ public function test_add_string_sql_statement(string $type, string $method, int| } $sql = $this->generator->{"add$method"}('name', ['default' => $default, 'size' => 100])->make(); if (preg_match('/STRING|VARCHAR/', $type)) { - $this->assertEquals($sql, "name {$type}(100) NOT NULL DEFAULT '$default'"); + $this->assertEquals($sql, "\"name\" {$type}(100) NOT NULL DEFAULT '$default'"); } $sql = $this->generator->{"add$method"}('name', ['default' => $default, 'size' => 100, 'nullable' => true])->make(); - $this->assertEquals($sql, "name {$type}(100) NULL DEFAULT '$default'"); + $this->assertEquals($sql, "\"name\" {$type}(100) NULL DEFAULT '$default'"); $sql = $this->generator->{"add$method"}('name', ['primary' => true])->make(); - $this->assertEquals($sql, "name {$type}(255) PRIMARY KEY NOT NULL"); + $this->assertEquals($sql, "\"name\" {$type}(255) PRIMARY KEY NOT NULL"); $sql = $this->generator->{"add$method"}('name', ['primary' => true, 'default' => $default, 'size' => 100, 'nullable' => true])->make(); - $this->assertEquals($sql, "name {$type}(100) PRIMARY KEY NULL DEFAULT '$default'"); + $this->assertEquals($sql, "\"name\" {$type}(100) PRIMARY KEY NULL DEFAULT '$default'"); $sql = $this->generator->{"add$method"}('name', ['unique' => true])->make(); - $this->assertEquals($sql, "name {$type}(255) UNIQUE NOT NULL"); + $this->assertEquals($sql, "\"name\" {$type}(255) UNIQUE NOT NULL"); } /** @@ -65,9 +65,9 @@ public function test_change_string_sql_statement(string $type, string $method, i $sql = $this->generator->{"change$method"}('name')->make(); if (preg_match('/STRING|VARCHAR/', $type)) { - $this->assertEquals($sql, "MODIFY COLUMN name {$type}(255) NOT NULL"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type}(255) NOT NULL"); } else { - $this->assertEquals($sql, "MODIFY COLUMN name {$type} NOT NULL"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type} NOT NULL"); } if (preg_match('/TEXT/', $type)) { @@ -76,20 +76,20 @@ public function test_change_string_sql_statement(string $type, string $method, i } $sql = $this->generator->{"change$method"}('name', ['default' => $default, 'size' => 100])->make(); if (preg_match('/STRING|VARCHAR/', $type)) { - $this->assertEquals($sql, "MODIFY COLUMN name {$type}(100) NOT NULL DEFAULT '$default'"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type}(100) NOT NULL DEFAULT '$default'"); } $sql = $this->generator->{"change$method"}('name', ['default' => $default, 'size' => 100, 'nullable' => true])->make(); - $this->assertEquals($sql, "MODIFY COLUMN name {$type}(100) NULL DEFAULT '$default'"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type}(100) NULL DEFAULT '$default'"); $sql = $this->generator->{"change$method"}('name', ['primary' => true])->make(); - $this->assertEquals($sql, "MODIFY COLUMN name {$type}(255) PRIMARY KEY NOT NULL"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type}(255) PRIMARY KEY NOT NULL"); $sql = $this->generator->{"change$method"}('name', ['primary' => true, 'default' => $default, 'size' => 100, 'nullable' => true])->make(); - $this->assertEquals($sql, "MODIFY COLUMN name {$type}(100) PRIMARY KEY NULL DEFAULT '$default'"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type}(100) PRIMARY KEY NULL DEFAULT '$default'"); $sql = $this->generator->{"change$method"}('name', ['unique' => true])->make(); - $this->assertEquals($sql, "MODIFY COLUMN name {$type}(255) UNIQUE NOT NULL"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type}(255) UNIQUE NOT NULL"); } /** @@ -101,41 +101,41 @@ public function test_add_string_without_size_sql_statement(string $type, string $sql = $this->generator->{"add$method"}('name')->make(); $this->assertNotEquals($sql, 'name STRING NOT NULL'); - $this->assertEquals($sql, "name {$type} NOT NULL"); + $this->assertEquals($sql, "\"name\" {$type} NOT NULL"); $sql = $this->generator->{"add$method"}('name', ['default' => $default])->make(); if ($type === "CHAR") { - $this->assertEquals($sql, "name {$type} NOT NULL DEFAULT '$default'"); + $this->assertEquals($sql, "\"name\" {$type} NOT NULL DEFAULT '$default'"); } else { - $this->assertEquals($sql, "name {$type} NOT NULL DEFAULT $default"); + $this->assertEquals($sql, "\"name\" {$type} NOT NULL DEFAULT $default"); } $sql = $this->generator->{"add$method"}('name', ['default' => $default, 'size' => 100])->make(); if ($type === "CHAR") { - $this->assertEquals($sql, "name {$type} NOT NULL DEFAULT '$default'"); + $this->assertEquals($sql, "\"name\" {$type} NOT NULL DEFAULT '$default'"); } else { - $this->assertEquals($sql, "name {$type} NOT NULL DEFAULT $default"); + $this->assertEquals($sql, "\"name\" {$type} NOT NULL DEFAULT $default"); } $sql = $this->generator->{"add$method"}('name', ['default' => $default, 'nullable' => true])->make(); if ($type === "CHAR") { - $this->assertEquals($sql, "name {$type} NULL DEFAULT '$default'"); + $this->assertEquals($sql, "\"name\" {$type} NULL DEFAULT '$default'"); } else { - $this->assertEquals($sql, "name {$type} NULL DEFAULT $default"); + $this->assertEquals($sql, "\"name\" {$type} NULL DEFAULT $default"); } $sql = $this->generator->{"add$method"}('name', ['primary' => true])->make(); - $this->assertEquals($sql, "name {$type} PRIMARY KEY NOT NULL"); + $this->assertEquals($sql, "\"name\" {$type} PRIMARY KEY NOT NULL"); $sql = $this->generator->{"add$method"}('name', ['primary' => true, 'default' => $default, 'nullable' => true])->make(); if ($type === "CHAR") { - $this->assertEquals($sql, "name {$type} PRIMARY KEY NULL DEFAULT '$default'"); + $this->assertEquals($sql, "\"name\" {$type} PRIMARY KEY NULL DEFAULT '$default'"); } else { - $this->assertEquals($sql, "name {$type} PRIMARY KEY NULL DEFAULT $default"); + $this->assertEquals($sql, "\"name\" {$type} PRIMARY KEY NULL DEFAULT $default"); } $sql = $this->generator->{"add$method"}('name', ['unique' => true])->make(); - $this->assertEquals($sql, "name {$type} UNIQUE NOT NULL"); + $this->assertEquals($sql, "\"name\" {$type} UNIQUE NOT NULL"); } /** @@ -146,41 +146,41 @@ public function test_change_string_without_size_sql_statement(string $type, stri $type = strtoupper($type); $sql = $this->generator->{"change$method"}('name')->make(); - $this->assertEquals($sql, "MODIFY COLUMN name {$type} NOT NULL"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type} NOT NULL"); $sql = $this->generator->{"change$method"}('name', ['default' => $default, 'size' => 100])->make(); if ($type === 'CHAR') { - $this->assertEquals($sql, "MODIFY COLUMN name {$type} NOT NULL DEFAULT '$default'"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type} NOT NULL DEFAULT '$default'"); } else { - $this->assertEquals($sql, "MODIFY COLUMN name {$type} NOT NULL DEFAULT $default"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type} NOT NULL DEFAULT $default"); } $sql = $this->generator->{"change$method"}('name', ['default' => $default])->make(); if ($type === 'CHAR') { - $this->assertEquals($sql, "MODIFY COLUMN name {$type} NOT NULL DEFAULT '$default'"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type} NOT NULL DEFAULT '$default'"); } else { - $this->assertEquals($sql, "MODIFY COLUMN name {$type} NOT NULL DEFAULT $default"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type} NOT NULL DEFAULT $default"); } $sql = $this->generator->{"change$method"}('name', ['default' => $default, 'nullable' => true])->make(); if ($type === 'CHAR') { - $this->assertEquals($sql, "MODIFY COLUMN name {$type} NULL DEFAULT '$default'"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type} NULL DEFAULT '$default'"); } else { - $this->assertEquals($sql, "MODIFY COLUMN name {$type} NULL DEFAULT $default"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type} NULL DEFAULT $default"); } $sql = $this->generator->{"change$method"}('name', ['primary' => true])->make(); - $this->assertEquals($sql, "MODIFY COLUMN name {$type} PRIMARY KEY NOT NULL"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type} PRIMARY KEY NOT NULL"); $sql = $this->generator->{"change$method"}('name', ['primary' => true, 'default' => $default, 'nullable' => true])->make(); if ($type === 'CHAR') { - $this->assertEquals($sql, "MODIFY COLUMN name {$type} PRIMARY KEY NULL DEFAULT '$default'"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type} PRIMARY KEY NULL DEFAULT '$default'"); } else { - $this->assertEquals($sql, "MODIFY COLUMN name {$type} PRIMARY KEY NULL DEFAULT $default"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type} PRIMARY KEY NULL DEFAULT $default"); } $sql = $this->generator->{"change$method"}('name', ['unique' => true])->make(); - $this->assertEquals($sql, "MODIFY COLUMN name {$type} UNIQUE NOT NULL"); + $this->assertEquals($sql, "MODIFY COLUMN \"name\" {$type} UNIQUE NOT NULL"); } /** @@ -189,32 +189,33 @@ public function test_change_string_without_size_sql_statement(string $type, stri public function test_add_int_sql_statement(string $type, string $method, int|string $default = 1) { $type = strtoupper($type); + $serial = in_array($type, ["INT", "TINYINT", "SMALLINT"]) ? "SERIAL" : "BIGSERIAL"; $sql = $this->generator->{"add$method"}('column')->make(); - $this->assertEquals($sql, "column {$type} NOT NULL"); + $this->assertEquals($sql, "\"column\" {$type} NOT NULL"); $sql = $this->generator->{"add$method"}('column', ['default' => $default, 'size' => 100])->make(); - $this->assertEquals($sql, "column {$type} NOT NULL DEFAULT $default"); + $this->assertEquals($sql, "\"column\" {$type} NOT NULL DEFAULT $default"); $sql = $this->generator->{"add$method"}('column', ['default' => $default, 'size' => 100, 'nullable' => true])->make(); - $this->assertEquals($sql, "column {$type} NULL DEFAULT $default"); + $this->assertEquals($sql, "\"column\" {$type} NULL DEFAULT $default"); $sql = $this->generator->{"add$method"}('column', ['primary' => true])->make(); - $this->assertEquals($sql, "column {$type} PRIMARY KEY NOT NULL"); + $this->assertEquals($sql, "\"column\" {$type} PRIMARY KEY NOT NULL"); $sql = $this->generator->{"add$method"}('column', ['primary' => true, 'default' => $default, 'size' => 100, 'nullable' => true])->make(); - $this->assertEquals($sql, "column {$type} PRIMARY KEY NULL DEFAULT $default"); + $this->assertEquals($sql, "\"column\" {$type} PRIMARY KEY NULL DEFAULT $default"); $sql = $this->generator->{"add$method"}('column', ['primary' => true, 'increment' => true, 'size' => 100, 'nullable' => true])->make(); - $this->assertEquals($sql, "column SERIAL PRIMARY KEY NULL"); + $this->assertEquals($sql, "\"column\" $serial PRIMARY KEY NULL"); $sql = $this->generator->{"add$method"}('column', ['unique' => true])->make(); - $this->assertEquals($sql, "column {$type} UNIQUE NOT NULL"); + $this->assertEquals($sql, "\"column\" {$type} UNIQUE NOT NULL"); $method = "add{$method}Increment"; if (method_exists($this->generator, $method)) { $sql = $this->generator->{$method}('column')->make(); - $this->assertEquals($sql, "column SERIAL PRIMARY KEY NOT NULL"); + $this->assertEquals($sql, "\"column\" $serial PRIMARY KEY NOT NULL"); } } @@ -224,42 +225,43 @@ public function test_add_int_sql_statement(string $type, string $method, int|str public function test_change_int_sql_statement(string $type, string $method, int|string $default = 1) { $type = strtoupper($type); + $serial = in_array($type, ["INT", "TINYINT", "SMALLINT"]) ? "SERIAL" : "BIGSERIAL"; $sql = $this->generator->{"change$method"}('column')->make(); - $this->assertEquals($sql, "MODIFY COLUMN column {$type} NOT NULL"); + $this->assertEquals($sql, "MODIFY COLUMN \"column\" {$type} NOT NULL"); $sql = $this->generator->{"change$method"}('column', ['default' => $default, 'size' => 100])->make(); - $this->assertEquals($sql, "MODIFY COLUMN column {$type} NOT NULL DEFAULT $default"); + $this->assertEquals($sql, "MODIFY COLUMN \"column\" {$type} NOT NULL DEFAULT $default"); $sql = $this->generator->{"change$method"}('column', ['default' => $default, 'size' => 100, 'nullable' => true])->make(); - $this->assertEquals($sql, "MODIFY COLUMN column {$type} NULL DEFAULT $default"); + $this->assertEquals($sql, "MODIFY COLUMN \"column\" {$type} NULL DEFAULT $default"); $sql = $this->generator->{"change$method"}('column', ['primary' => true])->make(); - $this->assertEquals($sql, "MODIFY COLUMN column {$type} PRIMARY KEY NOT NULL"); + $this->assertEquals($sql, "MODIFY COLUMN \"column\" {$type} PRIMARY KEY NOT NULL"); $sql = $this->generator->{"change$method"}('column', ['primary' => true, 'default' => $default, 'size' => 100, 'nullable' => true])->make(); - $this->assertEquals($sql, "MODIFY COLUMN column {$type} PRIMARY KEY NULL DEFAULT $default"); + $this->assertEquals($sql, "MODIFY COLUMN \"column\" {$type} PRIMARY KEY NULL DEFAULT $default"); $sql = $this->generator->{"change$method"}('column', ['primary' => true, 'increment' => true, 'size' => 100, 'nullable' => true])->make(); - $this->assertEquals($sql, "MODIFY COLUMN column SERIAL PRIMARY KEY NULL"); + $this->assertEquals($sql, "MODIFY COLUMN \"column\" $serial PRIMARY KEY NULL"); $sql = $this->generator->{"change$method"}('column', ['unique' => true])->make(); - $this->assertEquals($sql, "MODIFY COLUMN column {$type} UNIQUE NOT NULL"); + $this->assertEquals($sql, "MODIFY COLUMN \"column\" {$type} UNIQUE NOT NULL"); $method = "change{$method}Increment"; if (method_exists($this->generator, $method)) { $sql = $this->generator->{$method}('column')->make(); - $this->assertEquals($sql, "MODIFY COLUMN column {$type} SERIAL PRIMARY KEY NOT NULL"); + $this->assertEquals($sql, "MODIFY COLUMN \"column\" {$type} $serial PRIMARY KEY NOT NULL"); } } public function test_uuid_statement() { $sql = $this->generator->addUuid('column', ['unique' => true])->make(); - $this->assertEquals($sql, "column UUID UNIQUE NOT NULL DEFAULT uuid_generate_v4()"); + $this->assertEquals($sql, "\"column\" UUID UNIQUE NOT NULL"); $sql = $this->generator->addUuid('column', ['primary' => true])->make(); - $this->assertEquals($sql, "column UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4()"); + $this->assertEquals($sql, "\"column\" UUID PRIMARY KEY NOT NULL"); $this->expectException(SQLGeneratorException::class); $this->expectExceptionMessage("Cannot define the increment for uuid. You can use addUuidPrimary() instead"); @@ -269,7 +271,7 @@ public function test_uuid_statement() public function test_uuid_primary_statement() { $sql = $this->generator->addUuidPrimary('column')->make(); - $this->assertEquals($sql, "column UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4()"); + $this->assertEquals($sql, "\"column\" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4()"); } public function test_uuid_should_throw_errors_with_increment_attribute() @@ -286,7 +288,7 @@ public function getNumberTypes() ["bigint", "BigInteger", 1], ["tinyint", "TinyInteger", 1], ["float", "Float", 1], - ["double", "Double", 1], + ["double precision", "Double", 1], ["smallint", "SmallInteger", 1], ["mediumint", "MediumInteger", 1], ]; diff --git a/tests/Database/PaginationTest.php b/tests/Database/PaginationTest.php new file mode 100644 index 00000000..2b90cda2 --- /dev/null +++ b/tests/Database/PaginationTest.php @@ -0,0 +1,50 @@ +pagination = new Pagination( + next: 2, + previous: 0, + total: 3, + perPage: 10, + current: 1, + data: collect(['item1', 'item2', 'item3']) + ); + } + + public function test_next(): void + { + $this->assertSame(2, $this->pagination->next()); + } + + public function test_previous(): void + { + $this->assertSame(0, $this->pagination->previous()); + } + + public function test_current(): void + { + $this->assertSame(1, $this->pagination->current()); + } + + public function test_items(): void + { + $this->assertSame(['item1', 'item2', 'item3'], $this->pagination->items()->toArray()); + } + + public function test_total(): void + { + $this->assertSame(3, $this->pagination->total()); + } +} diff --git a/tests/Database/Query/DatabaseQueryTest.php b/tests/Database/Query/DatabaseQueryTest.php index 39fde5c1..1e42e41e 100644 --- a/tests/Database/Query/DatabaseQueryTest.php +++ b/tests/Database/Query/DatabaseQueryTest.php @@ -214,7 +214,7 @@ public function test_transaction_table(string $name) $database->insert("insert into pets values(:id, :name);", ["id" => 1, 'name' => 'Ploy']); $result = 0; - $database->startTransaction(function () use ($database, &$result) { + $database->transaction(function () use ($database, &$result) { $result = $database->delete("delete from pets where id = :id", ['id' => 1]); $this->assertEquals($database->inTransaction(), true); }); diff --git a/tests/Database/Query/PaginationTest.php b/tests/Database/Query/PaginationTest.php index adfa5203..d0487569 100644 --- a/tests/Database/Query/PaginationTest.php +++ b/tests/Database/Query/PaginationTest.php @@ -3,6 +3,7 @@ namespace Bow\Tests\Database\Query; use Bow\Database\Database; +use Bow\Database\Pagination; use Bow\Tests\Config\TestingConfiguration; class PaginationTest extends \PHPUnit\Framework\TestCase @@ -22,13 +23,13 @@ public function test_go_current_pagination(string $name) $this->createTestingTable($name); $result = Database::table("pets")->paginate(10); - $this->assertIsArray($result); - $this->assertEquals(count($result["data"]), 10); - $this->assertEquals($result["per_page"], 10); - $this->assertEquals($result["total"], 3); - $this->assertEquals($result["current"], 1); - $this->assertEquals($result["previous"], 1); - $this->assertEquals($result["next"], 2); + $this->assertInstanceOf(Pagination::class, $result); + $this->assertEquals(count($result->items()), 10); + $this->assertEquals($result->perPage(), 10); + $this->assertEquals($result->total(), 3); + $this->assertEquals($result->current(), 1); + $this->assertEquals($result->previous(), 1); + $this->assertEquals($result->next(), 2); } /** @@ -40,13 +41,13 @@ public function test_go_next_2_pagination(string $name) $this->createTestingTable($name); $result = Database::table("pets")->paginate(10, 2); - $this->assertIsArray($result); - $this->assertEquals(count($result["data"]), 10); - $this->assertEquals($result["per_page"], 10); - $this->assertEquals($result["total"], 3); - $this->assertEquals($result["current"], 2); - $this->assertEquals($result["previous"], 1); - $this->assertEquals($result["next"], 3); + $this->assertInstanceOf(Pagination::class, $result); + $this->assertEquals(count($result->items()), 10); + $this->assertEquals($result->perPage(), 10); + $this->assertEquals($result->total(), 3); + $this->assertEquals($result->current(), 2); + $this->assertEquals($result->previous(), 1); + $this->assertEquals($result->next(), 3); } /** @@ -58,13 +59,13 @@ public function test_go_next_3_pagination(string $name) $this->createTestingTable($name); $result = Database::table("pets")->paginate(10, 3); - $this->assertIsArray($result); - $this->assertEquals(count($result["data"]), 10); - $this->assertEquals($result["per_page"], 10); - $this->assertEquals($result["total"], 3); - $this->assertEquals($result["current"], 3); - $this->assertEquals($result["previous"], 2); - $this->assertEquals($result["next"], false); + $this->assertInstanceOf(Pagination::class, $result); + $this->assertEquals(count($result->items()), 10); + $this->assertEquals($result->perPage(), 10); + $this->assertEquals($result->total(), 3); + $this->assertEquals($result->current(), 3); + $this->assertEquals($result->previous(), 2); + $this->assertEquals($result->next(), false); } /** diff --git a/tests/Database/RedisTest.php b/tests/Database/RedisTest.php new file mode 100644 index 00000000..45f4fa2d --- /dev/null +++ b/tests/Database/RedisTest.php @@ -0,0 +1,30 @@ +assertEquals($result, true); + } + + public function test_get_cache() + { + Redis::set('lastname', 'papac'); + + $this->assertNull(Redis::get('name')); + $this->assertEquals(Redis::get('lastname'), "papac"); + } +} diff --git a/tests/Database/Relation/BelongsToRelationQueryTest.php b/tests/Database/Relation/BelongsToRelationQueryTest.php index d983bcb2..d17be65a 100644 --- a/tests/Database/Relation/BelongsToRelationQueryTest.php +++ b/tests/Database/Relation/BelongsToRelationQueryTest.php @@ -2,12 +2,13 @@ namespace Bow\Tests\Database\Relation; +use Bow\Cache\Cache; use Bow\Database\Database; use Bow\Database\Migration\SQLGenerator; use Bow\Tests\Config\TestingConfiguration; -use Bow\Tests\Database\Stubs\MigrationExtendedStub; -use Bow\Tests\Database\Stubs\PetMasterModelStub; use Bow\Tests\Database\Stubs\PetModelStub; +use Bow\Tests\Database\Stubs\PetMasterModelStub; +use Bow\Tests\Database\Stubs\MigrationExtendedStub; class BelongsToRelationQueryTest extends \PHPUnit\Framework\TestCase { @@ -15,6 +16,7 @@ public static function setUpBeforeClass(): void { $config = TestingConfiguration::getConfig(); Database::configure($config["database"]); + Cache::configure($config["cache"]); } public function connectionNames() diff --git a/tests/Filesystem/DiskFilesystemTest.php b/tests/Filesystem/DiskFilesystemTest.php index 06a083a3..0659af03 100644 --- a/tests/Filesystem/DiskFilesystemTest.php +++ b/tests/Filesystem/DiskFilesystemTest.php @@ -2,7 +2,7 @@ namespace Bow\Tests\Filesystem; -use Bow\Http\UploadFile; +use Bow\Http\UploadedFile; use Bow\Storage\Service\DiskFilesystemService; use Bow\Storage\Storage; use Bow\Tests\Config\TestingConfiguration; @@ -29,9 +29,9 @@ public function setUp(): void $this->storage = Storage::disk(); } - public function getUploadFileMock(): \PHPUnit\Framework\MockObject\MockObject + public function getUploadedFileMock(): \PHPUnit\Framework\MockObject\MockObject { - $uploadedFile = $this->getMockBuilder(UploadFile::class) + $uploadedFile = $this->getMockBuilder(UploadedFile::class) ->disableOriginalConstructor() ->getMock(); @@ -127,7 +127,7 @@ public function test_put_as_sucess() public function test_store() { - $uploadedFile = $this->getUploadFileMock(); + $uploadedFile = $this->getUploadedFileMock(); $filename = sprintf("%s.txt", md5(time())); $uploadedFile->method("getHashName")->willReturn($filename); @@ -138,7 +138,7 @@ public function test_store() public function test_store_on_custom_store() { - $uploadedFile = $this->getUploadFileMock(); + $uploadedFile = $this->getUploadedFileMock(); $filename = sprintf("%s.txt", md5(time())); $uploadedFile->method("getHashName")->willReturn($filename); @@ -149,7 +149,7 @@ public function test_store_on_custom_store() public function test_store_with_location_by_filename_setting() { - $uploadedFile = $this->getUploadFileMock(); + $uploadedFile = $this->getUploadedFileMock(); $filename = "stub_store_filename.txt"; $result = $this->storage->store($uploadedFile, "stores", [ @@ -162,7 +162,7 @@ public function test_store_with_location_by_filename_setting() public function test_store_with_location_as_null_and_filename_as_null() { - $uploadedFile = $this->getUploadFileMock(); + $uploadedFile = $this->getUploadedFileMock(); $filename = sprintf("%s.txt", md5(time())); $uploadedFile->method("getHashName")->willReturn($filename); @@ -175,7 +175,7 @@ public function test_store_with_location_as_null_and_filename_as_null() public function test_store_with_location_as_array_with_as_filename_key() { - $uploadedFile = $this->getUploadFileMock(); + $uploadedFile = $this->getUploadedFileMock(); $result = $this->storage->store($uploadedFile, [ "as" => "stub_store_filename.txt" diff --git a/tests/Filesystem/FTPServiceTest.php b/tests/Filesystem/FTPServiceTest.php index 2014cc70..ce08dae7 100644 --- a/tests/Filesystem/FTPServiceTest.php +++ b/tests/Filesystem/FTPServiceTest.php @@ -27,7 +27,7 @@ protected function setUp(): void protected function tearDown(): void { - $this->ftp_service->setConnectionRoot(); + $this->ftp_service->changePath(); } public function test_the_connection() @@ -159,7 +159,7 @@ public function test_append_content_into_file() $this->createFile($this->ftp_service, 'append.txt', 'something'); $this->ftp_service->append('append.txt', ' else'); - $this->assertRegExp('/something else/', $this->ftp_service->get('append.txt')); + $this->assertMatchesRegularExpression('/something else/', $this->ftp_service->get('append.txt')); } public function test_prepend_content_into_file() @@ -167,7 +167,7 @@ public function test_prepend_content_into_file() $this->createFile($this->ftp_service, 'prepend.txt', 'else'); $this->ftp_service->prepend('prepend.txt', 'something '); - $this->assertRegExp('/something else/', $this->ftp_service->get('prepend.txt')); + $this->assertMatchesRegularExpression('/something else/', $this->ftp_service->get('prepend.txt')); } public function test_put_content_into_file() @@ -180,7 +180,7 @@ public function test_put_content_into_file() private function createFile(FTPService $ftp_service, $filename, $content = '') { - $uploadedFile = $this->getMockBuilder(\Bow\Http\UploadFile::class) + $uploadedFile = $this->getMockBuilder(\Bow\Http\UploadedFile::class) ->disableOriginalConstructor() ->getMock(); diff --git a/tests/Mail/MailServiceTest.php b/tests/Mail/MailServiceTest.php index dddf1f79..2fe9daf3 100644 --- a/tests/Mail/MailServiceTest.php +++ b/tests/Mail/MailServiceTest.php @@ -110,8 +110,9 @@ public function test_send_mail_with_view_for_notive_driver() return $this->markTestSkipped('Test have been skip because /usr/sbin/sendmail not found'); } + $config = (array) $this->config["mail"]; View::configure($this->config["view"]); - Mail::configure([...$this->config["mail"], "driver" => "mail"]); + Mail::configure([...$config, "driver" => "mail"]); $response = Mail::send('mail', ['name' => "papac"], function (Message $message) { $message->to('bow@bowphp.com'); @@ -123,8 +124,10 @@ public function test_send_mail_with_view_for_notive_driver() public function test_send_mail_with_view_not_found_for_notive_driver() { + $config = (array) $this->config["mail"]; + View::configure($this->config["view"]); - Mail::configure([...$this->config["mail"], "driver" => "mail"]); + Mail::configure([...$config, "driver" => "mail"]); $this->expectException(ViewException::class); $this->expectExceptionMessage('The view [mail_view_not_found.twig] does not exists.'); diff --git a/tests/Queue/EventQueueTest.php b/tests/Queue/EventQueueTest.php new file mode 100644 index 00000000..31c52429 --- /dev/null +++ b/tests/Queue/EventQueueTest.php @@ -0,0 +1,49 @@ +boot(); + + static::$connection = new Connection($config["queue"]); + } + + /** + * @test + */ + public function it_should_queue_event() + { + $adapter = static::$connection->setConnection("beanstalkd")->getAdapter(); + $producer = new EventProducer(new UserEventListenerStub(), new UserEventStub("bowphp")); + $cache_filename = TESTING_RESOURCE_BASE_DIRECTORY . '/event.txt'; + + $adapter->push($producer); + $adapter->run(); + + $this->assertEquals("bowphp", file_get_contents($cache_filename)); + } +} diff --git a/tests/Queue/MailQueueTest.php b/tests/Queue/MailQueueTest.php new file mode 100644 index 00000000..52a14bab --- /dev/null +++ b/tests/Queue/MailQueueTest.php @@ -0,0 +1,46 @@ +boot(); + + static::$connection = new QueueConnection($config["queue"]); + } + + public function testQueueMail() + { + $message = new Message(); + $message->to("bow@bow.org"); + $message->subject("hello from bow"); + $producer = new MailQueueProducer("email", [], $message); + + $adapter = static::$connection->setConnection("beanstalkd")->getAdapter(); + + $adapter->push($producer); + $adapter->run(); + } +} diff --git a/tests/Queue/QueueTest.php b/tests/Queue/QueueTest.php index 53e16e64..6f26248a 100644 --- a/tests/Queue/QueueTest.php +++ b/tests/Queue/QueueTest.php @@ -2,11 +2,20 @@ namespace Bow\Tests\Queue; +use Bow\Cache\Adapter\RedisAdapter; +use Bow\Cache\CacheConfiguration; +use Bow\Configuration\EnvConfiguration; +use Bow\Configuration\LoggerConfiguration; use Bow\Database\Database; +use Bow\Database\DatabaseConfiguration; use Bow\Queue\Adapters\BeanstalkdAdapter; +use Bow\Queue\Adapters\DatabaseAdapter; +use Bow\Queue\Adapters\SQSAdapter; +use Bow\Queue\Adapters\SyncAdapter; use Bow\Tests\Config\TestingConfiguration; use Bow\Tests\Queue\Stubs\PetModelStub; use Bow\Queue\Connection as QueueConnection; +use Bow\Testing\KernelTesting; use Bow\Tests\Queue\Stubs\ModelProducerStub; use Bow\Tests\Queue\Stubs\BasicProducerStubs; @@ -16,43 +25,124 @@ class QueueTest extends \PHPUnit\Framework\TestCase public static function setUpBeforeClass(): void { + TestingConfiguration::withConfigurations([ + LoggerConfiguration::class, + DatabaseConfiguration::class, + CacheConfiguration::class, + EnvConfiguration::class, + ]); + $config = TestingConfiguration::getConfig(); - @unlink(TESTING_RESOURCE_BASE_DIRECTORY . '/producer.txt'); + $config->boot(); + static::$connection = new QueueConnection($config["queue"]); Database::connection('mysql'); Database::statement('drop table if exists pets'); Database::statement('create table pets (id int primary key auto_increment, name varchar(255))'); + Database::statement('create table if not exists queues ( + id varchar(255) primary key, + queue varchar(255), + payload text, + status varchar(100), + attempts int, + avalaibled_at datetime null default null, + reserved_at datetime null default null, + created_at datetime + )'); } - public function test_instance_of_adapter() + /** + * @dataProvider getConnection + * + * @param string $connection + * @return void + */ + public function test_instance_of_adapter($connection) { - $this->assertInstanceOf(BeanstalkdAdapter::class, static::$connection->getAdapter()); + $adapter = static::$connection->setConnection($connection)->getAdapter(); + + if ($connection == "beanstalkd") { + $this->assertInstanceOf(BeanstalkdAdapter::class, $adapter); + } elseif ($connection == "sqs") { + $this->assertInstanceOf(SQSAdapter::class, $adapter); + } elseif ($connection == "redis") { + $this->assertInstanceOf(RedisAdapter::class, $adapter); + } elseif ($connection == "database") { + $this->assertInstanceOf(DatabaseAdapter::class, $adapter); + } elseif ($connection == "sync") { + $this->assertInstanceOf(SyncAdapter::class, $adapter); + } } - public function test_push_service_adapter() + /** + * @dataProvider getConnection + * + * @param string $connection + * @return void + */ + public function test_push_service_adapter($connection) { - $adapter = static::$connection->getAdapter(); - $adapter->push(new BasicProducerStubs("running")); + $adapter = static::$connection->setConnection($connection)->getAdapter(); + $filename = TESTING_RESOURCE_BASE_DIRECTORY . "/{$connection}_producer.txt"; + + $adapter->push(new BasicProducerStubs($connection)); + $adapter->setQueue("queue_{$connection}"); + $adapter->setTries(3); + $adapter->setSleep(5); $adapter->run(); - $this->assertTrue(file_exists(TESTING_RESOURCE_BASE_DIRECTORY . '/producer.txt')); - $this->assertEquals(file_get_contents(TESTING_RESOURCE_BASE_DIRECTORY . '/producer.txt'), 'running'); + $this->assertTrue(file_exists($filename)); + $this->assertEquals(file_get_contents($filename), BasicProducerStubs::class); + + @unlink($filename); } - public function test_push_service_adapter_with_model() + /** + * @dataProvider getConnection + * @param string $connection + * @return void + */ + public function test_push_service_adapter_with_model($connection) { - $adapter = static::$connection->getAdapter(); + $adapter = static::$connection->setConnection($connection)->getAdapter(); $pet = new PetModelStub(["name" => "Filou"]); - $producer = new ModelProducerStub($pet); + $producer = new ModelProducerStub($pet, $connection); $adapter->push($producer); $adapter->run(); - $this->assertTrue(file_exists(TESTING_RESOURCE_BASE_DIRECTORY . '/producer.txt')); - $this->assertEquals(file_get_contents(TESTING_RESOURCE_BASE_DIRECTORY . '/producer.txt'), 'running'); + $this->assertTrue(file_exists(TESTING_RESOURCE_BASE_DIRECTORY . "/{$connection}_queue_pet_model_stub.txt")); + $content = file_get_contents(TESTING_RESOURCE_BASE_DIRECTORY . "/{$connection}_queue_pet_model_stub.txt"); + $data = json_decode($content); + $this->assertEquals($data->name, "Filou"); $pet = PetModelStub::first(); $this->assertNotNull($pet); + + @unlink(TESTING_RESOURCE_BASE_DIRECTORY . "/{$connection}_producer.txt"); + } + + /** + * Get the connection data + * + * @return array + */ + public function getConnection(): array + { + $data = [ + ["beanstalkd"], + ["database"], + ["sync"], + // ["sqs"], + // ["redis"], + // ["rabbitmq"] + ]; + + if (getenv("AWS_SQS_URL")) { + $data[] = ["sqs"]; + } + + return $data; } } diff --git a/tests/Queue/Stubs/BasicProducerStubs.php b/tests/Queue/Stubs/BasicProducerStubs.php index 911bf889..1f720027 100644 --- a/tests/Queue/Stubs/BasicProducerStubs.php +++ b/tests/Queue/Stubs/BasicProducerStubs.php @@ -6,15 +6,13 @@ class BasicProducerStubs extends ProducerService { - private ?string $name = null; - - public function __construct(string $name) - { - $this->name = $name; + public function __construct( + private string $connection + ) { } public function process(): void { - file_put_contents(TESTING_RESOURCE_BASE_DIRECTORY . '/producer.txt', $this->name); + file_put_contents(TESTING_RESOURCE_BASE_DIRECTORY . "/{$this->connection}_producer.txt", BasicProducerStubs::class); } } diff --git a/tests/Queue/Stubs/MixedProducerStub.php b/tests/Queue/Stubs/MixedProducerStub.php index eb8d85f8..ed17d1da 100644 --- a/tests/Queue/Stubs/MixedProducerStub.php +++ b/tests/Queue/Stubs/MixedProducerStub.php @@ -7,15 +7,14 @@ class MixedProducerStub extends ProducerService { - private ServiceStub $service; - - public function __construct(ServiceStub $service) - { - $this->service = $service; + public function __construct( + private ServiceStub $service, + private string $connection + ) { } public function process(): void { - $this->service->fire(); + $this->service->fire($this->connection); } } diff --git a/tests/Queue/Stubs/ModelProducerStub.php b/tests/Queue/Stubs/ModelProducerStub.php index 245fb29d..50ebe323 100644 --- a/tests/Queue/Stubs/ModelProducerStub.php +++ b/tests/Queue/Stubs/ModelProducerStub.php @@ -7,16 +7,20 @@ class ModelProducerStub extends ProducerService { - private PetModelStub $pet; - - public function __construct(PetModelStub $pet) - { + public function __construct( + private PetModelStub $pet, + private string $connection + ) { $this->pet = $pet; + $this->connection = $connection; } public function process(): void { $this->pet->save(); - file_put_contents(TESTING_RESOURCE_BASE_DIRECTORY . '/queue_pet_model_stub.txt', $this->pet->toJson()); + + file_put_contents(TESTING_RESOURCE_BASE_DIRECTORY . "/{$this->connection}_queue_pet_model_stub.txt", $this->pet->toJson()); + + $this->deleteJob(); } } diff --git a/tests/Queue/Stubs/ServiceStub.php b/tests/Queue/Stubs/ServiceStub.php index 173a6b89..e3979647 100644 --- a/tests/Queue/Stubs/ServiceStub.php +++ b/tests/Queue/Stubs/ServiceStub.php @@ -4,8 +4,14 @@ class ServiceStub { - public function fire(): void + /** + * The fire method + * + * @param string $connection + * @return void + */ + public function fire(string $connection): void { - file_put_contents(TESTING_RESOURCE_BASE_DIRECTORY . '/producer_service.txt', ServiceStub::class); + file_put_contents(TESTING_RESOURCE_BASE_DIRECTORY . "/{$connection}_producer_service.txt", ServiceStub::class); } } diff --git a/tests/Support/HttpClientTest.php b/tests/Support/HttpClientTest.php new file mode 100644 index 00000000..867a0430 --- /dev/null +++ b/tests/Support/HttpClientTest.php @@ -0,0 +1,38 @@ +get("https://www.oogle.com"); + + $this->assertEquals($response->statusCode(), 525); + } + + public function test_get_method_with_custom_headers() + { + $http = new HttpClient(); + + $http->addHeaders(["X-Api-Key" => "Fake-Key"]); + $response = $http->get("https://www.google.com"); + + $this->assertEquals($response->statusCode(), 200); + } + + public function test_should_be_fail_with_get_method() + { + $http = new HttpClient("https://www.google.com"); + + $http->addHeaders(["X-Api-Key" => "Fake-Key"]); + $response = $http->get("/the-fake-url"); + + $this->assertEquals($response->statusCode(), 404); + } +} diff --git a/tests/Support/StrTest.php b/tests/Support/StrTest.php index c0396396..68757027 100644 --- a/tests/Support/StrTest.php +++ b/tests/Support/StrTest.php @@ -4,7 +4,7 @@ use Bow\Support\Str; -class StrText extends \PHPUnit\Framework\TestCase +class StrTest extends \PHPUnit\Framework\TestCase { public function test_upper() { @@ -78,14 +78,14 @@ public function test_to_capitatize() $this->assertEquals(Str::capitalize('comment faire un site web avec php'), 'Comment Faire Un Site Web Avec Php'); } - public function test_randomize() + public function test_random() { - $this->assertEquals(strlen(Str::randomize(10)), 10); + $this->assertEquals(strlen(Str::random(10)), 10); } - public function test_get_words() + public function test_words() { - $this->assertEquals(Str::getWords('comment faire un site web avce php', 2), 'comment faire'); + $this->assertEquals(Str::words('comment faire un site web avce php', 2), 'comment faire'); } public function test_contains() diff --git a/tests/Support/TestingTest.php b/tests/Support/TestingTest.php new file mode 100644 index 00000000..5f10c275 --- /dev/null +++ b/tests/Support/TestingTest.php @@ -0,0 +1,40 @@ +get("/"); + + $response->assertStatus(200); + } + + public function test_get_method_with_custom_headers() + { + $this->withHeaders(["X-Api-Key" => "Fake-Key"]); + + $response = $this->get("/"); + + $response->assertStatus(200); + } + + public function test_should_be_fail_with_get_method() + { + $this->withHeaders(["X-Api-Key" => "Fake-Key"]); + + $response = $this->get("/the-fake-url-for-my-testing-please-do-not-block-this"); + + $response->assertStatus(404); + } +} diff --git a/tests/Translate/TranslationTest.php b/tests/Translate/TranslationTest.php index 5b10b64c..4f3f8a18 100644 --- a/tests/Translate/TranslationTest.php +++ b/tests/Translate/TranslationTest.php @@ -3,12 +3,14 @@ namespace Bow\Tests\Translate; use Bow\Translate\Translator; +use Bow\Tests\Config\TestingConfiguration; class TranslationTest extends \PHPUnit\Framework\TestCase { public static function setUpBeforeClass(): void { - Translator::configure('fr', __DIR__ . '/stubs'); + $config = TestingConfiguration::getConfig(); + Translator::configure($config['translate.lang'], $config["translate.dictionary"]); } public function test_fr_welcome_message() diff --git a/tests/Validation/ValidationTest.php b/tests/Validation/ValidationTest.php index bee5cba4..17105e4b 100644 --- a/tests/Validation/ValidationTest.php +++ b/tests/Validation/ValidationTest.php @@ -3,21 +3,17 @@ namespace Bow\Tests\Validation; use Bow\Database\Database; -use Bow\Tests\Config\TestingConfiguration; +use Bow\Translate\Translator; use Bow\Validation\Validator; +use Bow\Tests\Config\TestingConfiguration; class ValidationTest extends \PHPUnit\Framework\TestCase { - private static Database $database; - public static function setUpBeforeClass(): void { - static::$database = Database::getInstance(); - - if (!static::$database) { - $configuration = TestingConfiguration::getConfig(); - Database::configure($configuration["database"]); - } + $config = TestingConfiguration::getConfig(); + Database::configure($config["database"]); + Translator::configure($config['translate.lang'], $config["translate.dictionary"]); Database::statement("create table if not exists pets (id int primary key, name varchar(225));"); Database::table("pets")->truncate(); @@ -60,6 +56,15 @@ public function test_max_rule() $this->assertTrue($second_validation->fails()); } + public function test_min_rule() + { + $first_validation = Validator::make(['name' => 'bow'], ['name' => 'required|min:3']); + $second_validation = Validator::make(['name' => 'fr'], ['name' => 'required|min:5']); + + $this->assertFalse($first_validation->fails()); + $this->assertTrue($second_validation->fails()); + } + public function test_lower_rule() { $first_validation = Validator::make(['name' => 'bow'], ['name' => 'required|lower']); @@ -141,7 +146,7 @@ public function test_not_exists_rule() $this->assertFalse($second_validation->fails()); } - public function test_unique() + public function test_unique_rule() { Database::insert("insert into pets values(3, 'Couli');"); @@ -156,4 +161,22 @@ public function test_unique() $thrid_validation = Validator::make(['name' => 'Couli'], ['name' => 'required|unique:pets,name']); $this->assertTrue($thrid_validation->fails()); } + + public function test_required_rule() + { + $first_validation = Validator::make(['name' => 'Couli'], ['lastname' => 'required']); + $second_validation = Validator::make(['name' => 'Milou'], ['name' => 'required']); + + $this->assertTrue($first_validation->fails()); + $this->assertFalse($second_validation->fails()); + } + + public function test_required_if_rule() + { + $first_validation = Validator::make(['name' => 'Couli'], ['lastname' => 'required_if:username']); + $second_validation = Validator::make(['name' => 'Milou'], ['lastname' => 'required_if:name']); + + $this->assertFalse($first_validation->fails()); + $this->assertTrue($second_validation->fails()); + } } diff --git a/tests/View/stubs/email.twig b/tests/View/stubs/email.twig new file mode 100644 index 00000000..994d487a --- /dev/null +++ b/tests/View/stubs/email.twig @@ -0,0 +1,5 @@ +Hello, + +Bow framework is awesome + +Best,