From b9f998d16107ae313e389a52d6f442ce87a655e0 Mon Sep 17 00:00:00 2001 From: eliseekn Date: Sun, 13 Aug 2023 11:03:14 +0000 Subject: [PATCH] Update whole framework architecture Improve code readability Remove unnecessary code Add new features --- app/Database/Factories/TokenFactory.php | 4 +- app/Database/Factories/UserFactory.php | 4 +- app/Database/Models/Token.php | 28 ++- app/Database/Models/User.php | 15 +- app/Enums/TokenDescription.php | 2 +- app/Http/Actions/User/StoreAction.php | 2 +- app/Http/Actions/User/UpdateAction.php | 7 +- .../Auth/EmailVerificationController.php | 27 +-- .../Auth/ForgotPasswordController.php | 47 ++--- app/Http/Middlewares/RememberUser.php | 2 +- app/Mails/WelcomeMail.php | 4 +- bootstrap.php | 4 +- composer.json | 7 +- config/console.php | 5 +- config/errors.php | 1 + config/mailer.php | 2 +- config/security.php | 2 +- core/Application.php | 9 +- core/Console/Make/Make.php | 18 -- core/Console/Make/Repository.php | 47 ----- core/Database/Connection/Connection.php | 5 +- core/Database/Connection/MySQLConnection.php | 16 +- core/Database/Connection/SQLiteConnection.php | 33 ++-- core/Database/Factory.php | 21 +-- core/Database/Migration.php | 25 +-- core/Database/Model.php | 173 +++++++----------- core/Database/QueryBuilder.php | 22 +-- core/Database/Repository.php | 24 +-- core/Exceptions/InvalidSQLQueryException.php | 21 +++ core/Http/Client/Curl.php | 2 +- core/Http/Request.php | 6 - core/Routing/Route.php | 72 ++++---- core/Routing/Router.php | 8 +- core/Stubs/Controller.stub | 2 +- core/Stubs/Factory.stub | 4 +- core/Stubs/Model.stub | 20 +- core/Stubs/Repository.stub | 19 -- core/Stubs/actions/destroy.stub | 7 +- core/Stubs/actions/update.stub | 8 +- core/Support/Auth.php | 27 ++- core/Support/TwigExtensions.php | 4 +- core/Testing/ApplicationTestCase.php | 47 +++-- routes/api.php | 4 +- routes/auth.php | 30 ++- .../Auth/EmailVerificationTest.php | 8 +- tests/Application/Auth/PasswordForgotTest.php | 18 +- tests/bootstrap.php | 2 +- views/errors/403.html.twig | 13 ++ 48 files changed, 412 insertions(+), 466 deletions(-) delete mode 100644 core/Console/Make/Repository.php create mode 100644 core/Exceptions/InvalidSQLQueryException.php delete mode 100644 core/Stubs/Repository.stub create mode 100644 views/errors/403.html.twig diff --git a/app/Database/Factories/TokenFactory.php b/app/Database/Factories/TokenFactory.php index a150f2ca..14a0bb09 100644 --- a/app/Database/Factories/TokenFactory.php +++ b/app/Database/Factories/TokenFactory.php @@ -15,11 +15,9 @@ class TokenFactory extends Factory { - protected static $model = Token::class; - public function __construct(int $count = 1) { - parent::__construct($count); + parent::__construct(Token::class, $count); } public function data(): array diff --git a/app/Database/Factories/UserFactory.php b/app/Database/Factories/UserFactory.php index 83faee77..d03b3963 100644 --- a/app/Database/Factories/UserFactory.php +++ b/app/Database/Factories/UserFactory.php @@ -15,11 +15,9 @@ class UserFactory extends Factory { - protected static $model = User::class; - public function __construct(int $count = 1) { - parent::__construct($count); + parent::__construct(User::class, $count); } public function data(): array diff --git a/app/Database/Models/Token.php b/app/Database/Models/Token.php index 32e3094d..9b13d3d5 100644 --- a/app/Database/Models/Token.php +++ b/app/Database/Models/Token.php @@ -12,5 +12,31 @@ class Token extends Model { - protected static $table = 'tokens'; + public function __construct() + { + parent::__construct('tokens'); + } + + public static function findByValue(string $value): Model|false + { + return (new self())->findBy('value', $value); + } + + public static function exists(string $email, string $description): Model|false + { + $token = (new self()) + ->where('email', $email) + ->and('description', $description); + + return !$token->exists() ? false : $token->first(); + } + + public static function findLatest(string $email, string $description): Model|false + { + return (new self()) + ->where('email', $email) + ->and('description', $description) + ->newest() + ->first(); + } } diff --git a/app/Database/Models/User.php b/app/Database/Models/User.php index 06b8c0bd..0fb25e9f 100644 --- a/app/Database/Models/User.php +++ b/app/Database/Models/User.php @@ -12,5 +12,18 @@ class User extends Model { - protected static $table = 'users'; + public function __construct() + { + parent::__construct('users'); + } + + public static function findByEmail(string $email): Model|false + { + return (new self())->findBy('email', $email); + } + + public static function findAllWhereEmailLike(string $email): array|false + { + return (new self())->where('email', 'like', $email)->getAll(); + } } diff --git a/app/Enums/TokenDescription.php b/app/Enums/TokenDescription.php index 539eaee7..2607e66a 100644 --- a/app/Enums/TokenDescription.php +++ b/app/Enums/TokenDescription.php @@ -5,7 +5,7 @@ enum TokenDescription: string { case PASSWORD_RESET_TOKEN = 'password_reset_token'; - case EMAIL_VERIFICATION_TOKEN = 'email_verifications_token'; + case EMAIL_VERIFICATION_TOKEN = 'email_verification_token'; public static function values(): array { diff --git a/app/Http/Actions/User/StoreAction.php b/app/Http/Actions/User/StoreAction.php index acab38d1..3570f70f 100644 --- a/app/Http/Actions/User/StoreAction.php +++ b/app/Http/Actions/User/StoreAction.php @@ -16,6 +16,6 @@ class StoreAction public function handle(array $data): Model|false { $data['password'] = hash_pwd($data['password']); - return User::create($data); + return (new User())->create($data); } } diff --git a/app/Http/Actions/User/UpdateAction.php b/app/Http/Actions/User/UpdateAction.php index d983af8e..0928b984 100644 --- a/app/Http/Actions/User/UpdateAction.php +++ b/app/Http/Actions/User/UpdateAction.php @@ -15,9 +15,9 @@ class UpdateAction { public function handle(array $data, string $email): Model|false { - $user = User::findBy('email', $email); + $user = User::findByEmail($email); - if ($user === false) { + if (!$user) { return false; } @@ -25,7 +25,6 @@ public function handle(array $data, string $email): Model|false $data['password'] = hash_pwd($data['password']); } - $user->fill($data); - return $user->save(); + return $user->fill($data)->save(); } } diff --git a/app/Http/Controllers/Auth/EmailVerificationController.php b/app/Http/Controllers/Auth/EmailVerificationController.php index 8ce5d813..c42adc83 100644 --- a/app/Http/Controllers/Auth/EmailVerificationController.php +++ b/app/Http/Controllers/Auth/EmailVerificationController.php @@ -25,28 +25,24 @@ class EmailVerificationController extends Controller { public function notify(): void { - $token = generate_token(); + $tokenValue = generate_token(); - if (!Mail::send(new VerificationMail($this->request->queries('email'), $token))) { + if (!Mail::send(new VerificationMail($this->request->queries('email'), $tokenValue))) { Alert::default(__('email_verification_link_not_sent'))->error(); $this->render('auth.signup'); } - if ( - !Token::where('email', $this->request->queries('email')) - ->and('description', TokenDescription::EMAIL_VERIFICATION_TOKEN->value) - ->exists() - ) { - Token::create([ + $token = Token::exists($this->request->queries('email'), TokenDescription::EMAIL_VERIFICATION_TOKEN->value); + + if ($token) { + $token->update(['value' => $tokenValue]); + } else { + $token->fill([ 'email'=> $this->request->queries('email'), 'value' => $token, 'expire' => Carbon::now()->addDay()->toDateTimeString(), 'description' => TokenDescription::EMAIL_VERIFICATION_TOKEN->value - ]); - } else { - Token::where('email', $this->request->queries('email')) - ->and('description', TokenDescription::EMAIL_VERIFICATION_TOKEN->value) - ->update(['value' => $token]); + ])->save(); } Alert::default(__('email_verification_link_sent'))->success(); @@ -59,10 +55,7 @@ public function verify(UpdateAction $action): void $this->response(__('bad_request'), 400); } - $token = Token::where('email', $this->request->queries('email')) - ->and('description', TokenDescription::EMAIL_VERIFICATION_TOKEN->value) - ->newest() - ->first(); + $token = Token::findLatest($this->request->queries('email'), TokenDescription::EMAIL_VERIFICATION_TOKEN->value); if (!$token || $token->attribute('value') !== $this->request->queries('token')) { $this->response(__('invalid_password_reset_link'), 400); diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 9957ab95..11734b56 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -25,31 +25,27 @@ class ForgotPasswordController extends Controller { public function notify(): void { - $token = generate_token(); + $tokenValue = generate_token(); - if (Mail::send(new TokenMail($this->request->get('email'), $token))) { - if ( - !Token::where('email', $this->request->inputs('email')) - ->and('description', TokenDescription::PASSWORD_RESET_TOKEN->value) - ->exists() - ) { - Token::create([ - 'email'=> $this->request->inputs('email'), - 'value' => $token, - 'expire' => Carbon::now()->addHour()->toDateTimeString(), - 'description' => TokenDescription::PASSWORD_RESET_TOKEN->value - ]); - } else { - Token::where('email', $this->request->inputs('email')) - ->and('description', TokenDescription::PASSWORD_RESET_TOKEN->value) - ->update(['value' => $token]); - } + if (!Mail::send(new TokenMail($this->request->get('email'), $tokenValue))) { + Alert::default(__('password_reset_link_not_sent'))->error(); + $this->redirectBack(); + } - Alert::default(__('password_reset_link_sent'))->success(); - $this->redirectBack(); - } - - Alert::default(__('password_reset_link_not_sent'))->error(); + $token = Token::exists($this->request->queries('email'), TokenDescription::PASSWORD_RESET_TOKEN->value); + + if ($token) { + $token->update(['value' => $tokenValue]); + } else { + $token->fill([ + 'email'=> $this->request->queries('email'), + 'value' => $token, + 'expire' => Carbon::now()->addHour()->toDateTimeString(), + 'description' => TokenDescription::PASSWORD_RESET_TOKEN->value + ])->save(); + } + + Alert::default(__('password_reset_link_sent'))->success(); $this->redirectBack(); } @@ -59,10 +55,7 @@ public function reset(): void $this->response(__('bad_request'), 400); } - $token = Token::where('email', $this->request->queries('email')) - ->and('description', TokenDescription::PASSWORD_RESET_TOKEN->value) - ->newest() - ->first(); + $token = Token::findLatest($this->request->queries('email'), TokenDescription::PASSWORD_RESET_TOKEN->value); if (!$token || $token->attribute('value') !== $this->request->queries('token')) { $this->response(__('invalid_password_reset_link'), 400); diff --git a/app/Http/Middlewares/RememberUser.php b/app/Http/Middlewares/RememberUser.php index c5ce0adc..9e033ac1 100644 --- a/app/Http/Middlewares/RememberUser.php +++ b/app/Http/Middlewares/RememberUser.php @@ -20,7 +20,7 @@ class RememberUser public function handle(): void { if (Cookies::has('user')) { - $user = User::findBy('email', Cookies::get('user')); + $user = (new User())->findBy('email', Cookies::get('user')); if ($user !== false) { Session::create('user', $user); diff --git a/app/Mails/WelcomeMail.php b/app/Mails/WelcomeMail.php index b5396a37..bf2a988a 100644 --- a/app/Mails/WelcomeMail.php +++ b/app/Mails/WelcomeMail.php @@ -16,7 +16,7 @@ */ class WelcomeMail extends Mailer { - public function __construct(string $email, string $username) + public function __construct(string $email, string $name) { parent::__construct(); @@ -24,6 +24,6 @@ public function __construct(string $email, string $username) ->from(config('mailer.sender.email'), config('mailer.sender.name')) ->reply(config('mailer.sender.email'), config('mailer.sender.name')) ->subject('Welcome') - ->body(View::getContent('emails.welcome', compact('username'))); + ->body(View::getContent('emails.welcome', compact('name'))); } } diff --git a/bootstrap.php b/bootstrap.php index c6d49dc4..6495f9f6 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -15,8 +15,8 @@ * Setup application */ -define('DS', DIRECTORY_SEPARATOR); -define('APP_ROOT', __DIR__ . DS); +const DS = DIRECTORY_SEPARATOR; +const APP_ROOT = __DIR__ . DS; set_time_limit(0); diff --git a/composer.json b/composer.json index bf011700..f4fd569a 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,9 @@ { "name": "eliseekn/tinymvc", - "description": "TinyMVC is a PHP framework based on MVC architecture that helps you build easily and quickly powerful web applications and REST API.", + "description": "TinyMVC is a PHP framework based on MVC architecture that helps you build easily and quickly powerful web applications and RESTful API.", "type": "project", "license": "MIT", - "keywords": ["php", "framework", "mvc"], + "keywords": ["php-framework", "mvc"], "authors": [ { "name": "N'Guessan Kouadio Elisée", @@ -20,7 +20,8 @@ ], "psr-4": { "App\\" : "app/", - "Core\\" : "core/" + "Core\\" : "core/", + "Tests\\" : "tests/" } }, "config": { diff --git a/config/console.php b/config/console.php index 328101a5..64abd787 100644 --- a/config/console.php +++ b/config/console.php @@ -10,7 +10,7 @@ * Console commands */ - return [ +return [ 'core' => [ new \Core\Console\Database\Create(), new \Core\Console\Database\Delete(), @@ -28,7 +28,6 @@ new \Core\Console\Make\Validator(), new \Core\Console\Make\Seed(), new \Core\Console\Make\Factory(), - new \Core\Console\Make\Repository(), new \Core\Console\Make\View(), new \Core\Console\Make\Mail(), new \Core\Console\Make\Middleware(), @@ -56,4 +55,4 @@ 'app' => [ // ] - ]; \ No newline at end of file +]; \ No newline at end of file diff --git a/config/errors.php b/config/errors.php index f7fb896d..ce49fcb7 100644 --- a/config/errors.php +++ b/config/errors.php @@ -15,6 +15,7 @@ 'log' => true, 'views' => [ + '403' => 'errors' . DS . '403', '404' => 'errors' . DS . '404', '500' => 'errors' . DS . '500' ] diff --git a/config/mailer.php b/config/mailer.php index 61e318ac..cbff6e3c 100644 --- a/config/mailer.php +++ b/config/mailer.php @@ -15,7 +15,7 @@ 'sender' => [ 'name' => config('app.name'), - 'email' => 'tiny@mvc.framework', + 'email' => 'no-reply@tiny.mvc', ], 'smtp' => [ diff --git a/config/security.php b/config/security.php index 3849f9e0..b4c71fa7 100644 --- a/config/security.php +++ b/config/security.php @@ -19,7 +19,7 @@ 'auth' => [ 'max_attempts' => false, 'unlock_timeout' => 1, //in minute - 'email_verification' => true, + 'email_verification' => false, ], 'session' => [ diff --git a/core/Application.php b/core/Application.php index 7db95709..186b7bfd 100644 --- a/core/Application.php +++ b/core/Application.php @@ -31,8 +31,13 @@ public function run(): void try { Router::dispatch(new Request(), $response); } catch (Exception $e) { - if (config('errors.log')) save_log('Exception: ' . $e); - if (config('errors.display')) die($e); + if (config('errors.log')) { + save_log('Exception: ' . $e); + } + + if (config('errors.display')) { + die($e); + } $response->view(config('errors.views.500'))->send(500); } diff --git a/core/Console/Make/Make.php b/core/Console/Make/Make.php index cf31036f..64b9b835 100644 --- a/core/Console/Make/Make.php +++ b/core/Console/Make/Make.php @@ -163,25 +163,7 @@ public static function createFactory(string $factory, ?string $namespace = null) return $storage->writeFile(self::fixPluralTypo($class, true) . '.php', $data); } - - public static function createRepository(string $repository, ?string $namespace = null): bool - { - list($name, $class) = self::generateClass($repository, 'repository', true, true); - $data = self::stubs()->readFile('Repository.stub'); - $data = self::addNamespace($data, 'App\Database\Repositories', $namespace); - $data = str_replace('CLASSNAME', self::fixPluralTypo($class, true), $data); - $data = str_replace('MODELNAME', self::fixPluralTypo(ucfirst($name), true), $data); - - $storage = Storage::path(config('storage.repositories')); - - if (!is_null($namespace)) { - $storage = $storage->addPath(str_replace('\\', '/', $namespace)); - } - - return $storage->writeFile(self::fixPluralTypo($class, true) . '.php', $data); - } - public static function createHelper(string $helper): bool { list(, $class) = self::generateClass($helper, 'helper', true); diff --git a/core/Console/Make/Repository.php b/core/Console/Make/Repository.php deleted file mode 100644 index 27fc1261..00000000 --- a/core/Console/Make/Repository.php +++ /dev/null @@ -1,47 +0,0 @@ -setDescription('Create new model repository'); - $this->addArgument('repository', InputArgument::REQUIRED|InputArgument::IS_ARRAY, 'The name of model repository table (separated by space if many)'); - $this->addOption('namespace', null, InputOption::VALUE_OPTIONAL, 'Specify namespace (base: App\Database\Repositories)'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $repositories = $input->getArgument('repository'); - - foreach ($repositories as $repository) { - list(, $class) = Make::generateClass($repository, 'repository', true, true); - - if (!Make::createRepository($repository, $input->getOption('namespace'))) { - $output->writeln('Failed to create repository "' . Make::fixPluralTypo($class, true) . '"'); - } - - $output->writeln('repository "' . Make::fixPluralTypo($class, true) . '" has been created'); - } - - return Command::SUCCESS; - } -} diff --git a/core/Database/Connection/Connection.php b/core/Database/Connection/Connection.php index 49f63496..5a860096 100644 --- a/core/Database/Connection/Connection.php +++ b/core/Database/Connection/Connection.php @@ -19,9 +19,8 @@ class Connection /** * @var \Core\Database\Connection\Connection */ - protected static $instance = null; - - protected $db; + protected static ?Connection $instance = null; + protected MySQLConnection|SQLiteConnection $db; private function __construct() { diff --git a/core/Database/Connection/MySQLConnection.php b/core/Database/Connection/MySQLConnection.php index b1c7568b..76909f4e 100644 --- a/core/Database/Connection/MySQLConnection.php +++ b/core/Database/Connection/MySQLConnection.php @@ -39,6 +39,13 @@ public function getPDO(): PDO return $this->pdo; } + private function getDB(): string + { + return config('app.env') === 'test' + ? config('tests.database.suffix') + : config('database.name'); + } + /** * @throws PDOException */ @@ -60,7 +67,7 @@ public function executeQuery(string $query, ?array $args = null): false|PDOState $stmt = $this->pdo->prepare(trim($query)); $stmt->execute($args); } catch (PDOException $e) { - throw new PDOException($e->getMessage(), (int) $e->getCode(), $e->getPrevious()); + throw new PDOException($e->getMessage(), (int) $e->getCode(), $e->getPrevious()); } return $stmt; @@ -97,11 +104,4 @@ public function deleteSchema(string $name): void { $this->executeStatement("DROP DATABASE IF EXISTS $name"); } - - private function getDB(): string - { - return config('app.env') === 'test' - ? config('tests.database.suffix') - : config('database.name'); - } } diff --git a/core/Database/Connection/SQLiteConnection.php b/core/Database/Connection/SQLiteConnection.php index 0d78d9bd..8d44b882 100644 --- a/core/Database/Connection/SQLiteConnection.php +++ b/core/Database/Connection/SQLiteConnection.php @@ -15,10 +15,7 @@ class SQLiteConnection implements ConnectionInterface { - /** - * @var PDO - */ - protected $pdo; + protected PDO $pdo; /** * @throws PDOException @@ -38,7 +35,12 @@ public function __construct() } } - private function getDB() + public function getPDO(): PDO + { + return $this->pdo; + } + + private function getDB(): string { if (config('app.env') === 'test') { return config('storage.sqlite') . config('database.name') . config('tests.database.suffix') . '.db'; @@ -48,11 +50,6 @@ private function getDB() : config('storage.sqlite') . config('database.name') . '.db'; } - public function getPDO(): PDO - { - return $this->pdo; - } - /** * @throws PDOException */ @@ -82,11 +79,7 @@ public function executeQuery(string $query, ?array $args = null): false|PDOState public function schemaExists(string $name): bool { - if (config('database.sqlite.memory')) { - return true; - } - - return Storage::path(config('storage.sqlite'))->isFile($name); + return config('database.sqlite.memory') || Storage::path(config('storage.sqlite'))->isFile($name); } public function tableExists(string $name): bool @@ -97,17 +90,15 @@ public function tableExists(string $name): bool public function createSchema(string $name): void { - if (config('database.sqlite.memory')) { - return; + if (!config('database.sqlite.memory')) { + Storage::path(config('storage.sqlite'))->writeFile($name . '.db', ''); } - Storage::path(config('storage.sqlite'))->writeFile($name . '.db', ''); } public function deleteSchema(string $name): void { - if (config('database.sqlite.memory')) { - return; + if (!config('database.sqlite.memory')) { + Storage::path(config('storage.sqlite'))->deleteFile($name . '.db'); } - Storage::path(config('storage.sqlite'))->deleteFile($name . '.db'); } } diff --git a/core/Database/Factory.php b/core/Database/Factory.php index 3992bc12..65ae5df9 100644 --- a/core/Database/Factory.php +++ b/core/Database/Factory.php @@ -16,20 +16,15 @@ */ class Factory { - protected static $model; - protected array|Model $class; - public Generator $faker; + protected array $class; + protected Generator $faker; - public function __construct(int $count) + public function __construct(string $model, int $count) { $this->faker = FakerFactory::create(config('app.lang')); - if ($count === 1) { - $this->class = new static::$model(); - } else { - for ($i = 1; $i <= $count; $i++) { - $this->class[] = new static::$model(); - } + for ($i = 1; $i <= $count; $i++) { + $this->class[] = new $model(); } } @@ -40,9 +35,9 @@ public function data(): array public function make(array $data = []): mixed { - if (!is_array($this->class)) { - $this->class->fill(array_merge($this->data(), $data)); - return $this->class; + if (count($this->class) === 1) { + $this->class[0]->fill(array_merge($this->data(), $data)); + return $this->class[0]; } return array_map(function ($model) use ($data) { diff --git a/core/Database/Migration.php b/core/Database/Migration.php index 08661e64..7a62ccb4 100644 --- a/core/Database/Migration.php +++ b/core/Database/Migration.php @@ -18,7 +18,7 @@ class Migration /** * @var \Core\Database\QueryBuilder */ - protected static mixed $qb; + protected static QueryBuilder $qb; public static function driver(): string { @@ -29,19 +29,19 @@ public static function driver(): string public static function createTable(string $name): self { - self::$qb = QueryBuilder::createTable($name); + static::$qb = QueryBuilder::createTable($name); return new self(); } public static function addColumn(string $table): self { - self::$qb = QueryBuilder::addColumn($table); + static::$qb = QueryBuilder::addColumn($table); return new self(); } public static function renameColumn(string $table, string $old, string $new): self { - self::$qb = QueryBuilder::renameColumn($table, $old, $new); + static::$qb = QueryBuilder::renameColumn($table, $old, $new); return new self(); } @@ -50,13 +50,13 @@ public static function renameColumn(string $table, string $old, string $new): se */ public static function updateColumn(string $table, string $column): self { - self::$qb = QueryBuilder::updateColumn($table, $column); + static::$qb = QueryBuilder::updateColumn($table, $column); return new self(); } public static function deleteColumn(string $table, string $column): self { - self::$qb = QueryBuilder::deleteColumn($table, $column); + static::$qb = QueryBuilder::deleteColumn($table, $column); return new self(); } @@ -72,13 +72,13 @@ public static function dropForeign(string $table, string $name): false|PDOStatem public static function disableForeignKeyCheck(): false|PDOStatement { - $query = self::driver() === 'mysql' ? 'SET foreign_key_checks = 0' : 'PRAGMA foreign_keys = OFF'; + $query = static::driver() === 'mysql' ? 'SET foreign_key_checks = 0' : 'PRAGMA foreign_keys = OFF'; return QueryBuilder::setQuery($query)->execute(); } public static function enableForeignKeyCheck(): false|PDOStatement { - $query = self::driver() === 'mysql' ? 'SET foreign_key_checks = 1' : 'PRAGMA foreign_keys = ON'; + $query = static::driver() === 'mysql' ? 'SET foreign_key_checks = 1' : 'PRAGMA foreign_keys = ON'; return QueryBuilder::setQuery($query)->execute(); } @@ -322,13 +322,8 @@ public function default($default): self return $this; } - public function migrate(bool $table = true) + public function migrate(bool $table = true): false|PDOStatement { - if ($table) { - self::$qb->migrate(); - return; - } - - return self::$qb->flush()->execute(); + return $table ? self::$qb->migrate() : self::$qb->flush()->execute(); } } diff --git a/core/Database/Model.php b/core/Database/Model.php index e0e861a4..718c2467 100644 --- a/core/Database/Model.php +++ b/core/Database/Model.php @@ -15,140 +15,111 @@ */ class Model { - protected static $table = ''; - protected array $attributes = []; + protected Repository $repository; - public function __construct($data = []) + public function __construct(protected readonly string $table, protected array $attributes = []) { - $this->attributes = $data; + $this->repository = new Repository($table); } - public static function findBy(string $column, $operator = null, $value = null): self|false + public function findBy(string $column, $operator = null, $value = null): self|false { - return (new Repository(static::$table))->findWhere($column, $operator, $value); + return $this->repository->findWhere($column, $operator, $value); } - public static function find(int $id): self|false + public function find(int $id): self|false { - return self::findBy('id', $id); + return $this->findBy('id', $id); } - public static function all(): array|false + public function getAll(): array|false { - return (new Repository(static::$table))->selectAll('*'); + return $this->repository->selectAll('*'); } - public static function first(): Model|false + public function first(): Model|false { - return self::select('*')->first(); + return $this->select('*')->first(); } - public static function last(): Model|false + public function last(): Model|false { - return self::select('*')->last(); + return $this->select('*')->last(); } - public static function take(int $count, $subquery = null): array|false + public function take(int $count, $subquery = null): array|false { - return self::select('*')->subQuery($subquery)->take($count); + return $this->select('*')->subQuery($subquery)->take($count); } - public static function oldest(string $column = 'created_at', $subquery = null): array|false + public function oldest(string $column = 'created_at', $subquery = null): array|false { - return self::select('*')->subQuery($subquery)->oldest($column)->getAll(); + return $this->select('*')->subQuery($subquery)->oldest($column)->getAll(); } - public static function newest(string $column = 'created_at', $subquery = null): array|false + public function newest(string $column = 'created_at', $subquery = null): array|false { - return self::select('*')->subQuery($subquery)->newest($column)->getAll(); + return $this->select('*')->subQuery($subquery)->newest($column)->getAll(); } - public static function latest(string $column = 'id', $subquery = null): array|false + public function latest(string $column = 'id', $subquery = null): array|false { - return self::select('*')->subQuery($subquery)->latest($column)->getAll(); + return $this->select('*')->subQuery($subquery)->latest($column)->getAll(); } - public static function select(array|string $columns): Repository + public function select(array|string $columns): Repository { - return (new Repository(static::$table))->select($columns); + return $this->repository->select($columns); } - public static function where(string $column, $operator = null, $value = null): Repository + public function where(string $column, $operator = null, $value = null): Repository { - return self::select('*')->where($column, $operator, $value); + return $this->select('*')->where($column, $operator, $value); } - public static function count(string $column = 'id', $subquery = null): mixed + public function count(string $column = 'id', $subquery = null): mixed { - $data = (new Repository(static::$table))->count($column)->subQuery($subquery)->get(); - - if (!$data) { - return false; - } - - return $data->attribute('value'); + $data = $this->repository->count($column)->subQuery($subquery)->get(); + return !$data ? false : $data->attribute('value'); } - public static function sum(string $column, $subquery = null): mixed + public function sum(string $column, $subquery = null): mixed { - $data = (new Repository(static::$table))->sum($column)->subQuery($subquery)->get(); - - if (!$data) { - return false; - } - - return $data->attribute('value'); + $data = $this->repository->sum($column)->subQuery($subquery)->get(); + return !$data ? false : $data->attribute('value'); } - public static function max(string $column, $subquery = null): mixed + public function max(string $column, $subquery = null): mixed { - $data = (new Repository(static::$table))->max($column)->subQuery($subquery)->get(); - - if (!$data) { - return false; - } - - return $data->attribute('value'); + $data = $this->repository->max($column)->subQuery($subquery)->get(); + return !$data ? false : $data->attribute('value'); } - public static function min(string $column, $subquery = null): mixed + public function min(string $column, $subquery = null): mixed { - $data = (new Repository(static::$table))->min($column)->subQuery($subquery)->get(); - - if (!$data) { - return false; - } - - return $data->attribute('value'); + $data = $this->repository->min($column)->subQuery($subquery)->get(); + return !$data ? false : $data->attribute('value'); } - public static function metrics(string $column, string $type, string $period, int $interval = 0, ?array $query = null): mixed + public function metrics(string $column, string $type, string $period, int $interval = 0, ?array $query = null): mixed { - return (new Repository(static::$table))->metrics($column, $type, $period, $interval, $query); + return $this->repository->metrics($column, $type, $period, $interval, $query); } - public static function trends(string $column, string $type, string $period, int $interval = 0, ?array $query = null): array + public function trends(string $column, string $type, string $period, int $interval = 0, ?array $query = null): array { - return (new Repository(static::$table))->trends($column, $type, $period, $interval, $query); + return $this->repository->trends($column, $type, $period, $interval, $query); } - public static function create(array $data): self|false + public function create(array $data): self|false { - $id = (new Repository(static::$table))->insertGetId($data); - - if (is_null($id)) { - return false; - } - - return self::find($id); + $id = $this->repository->insertGetId($data); + return is_null($id) ? false : $this->find($id); } - /** - * Delete all rows - */ - public static function truncate(): false|PDOStatement + public function truncate(): false|PDOStatement { - return (new Repository(static::$table))->delete()->execute(); + return $this->repository->delete()->execute(); } public function getId(): int @@ -157,27 +128,19 @@ public function getId(): int } /** - * Get relationship of the model - * - * @param string $table - * @param mixed $column - * @return \Core\Database\Repository + * Get relationship of the model */ public function has(string $table, ?string $column = null): Repository { if (is_null($column)) { - $column = $this->getColumnFromTable(static::$table); + $column = $this->getColumnFromTable($this->table); } return (new Repository($table))->select('*')->where($column, $this->attributes['id']); } - + /** * Get relationship belongs to the model - * - * @param string $table - * @param mixed $column - * @return \Core\Database\Repository */ public function belongsTo(string $table, ?string $column = null): Repository { @@ -188,43 +151,41 @@ public function belongsTo(string $table, ?string $column = null): Repository return (new Repository($table))->select('*')->where('id', $this->attributes[$column]); } - public function attribute(string $key, $value = null): mixed + public function attribute(string $key): mixed { - if (!is_null($value)) { - $this->attributes[$key] = $value; - } - return $this->attributes[$key]; } - - /** - * Fill model attributes with custom data - * - * @param array $data - * @return void - */ - public function fill(array $data): void + + public function fill(array $data): self { foreach ($data as $key => $value) { $this->attributes[$key] = $value; } + + return $this; } - public function update(array $data): false|self + public function update(array $data): bool { - return !(new Repository(static::$table))->updateIfExists($this->attributes['id'], $data) ? false : $this; + return $this->repository->updateIfExists($this->attributes['id'], $data); } public function delete(): bool { - return (new Repository(static::$table))->deleteIfExists($this->attributes['id']); + return $this->repository->deleteIfExists($this->attributes['id']); } - public function save(): self|false + public function save(): Model|false { - return empty($this->attributes['id']) - ? self::create($this->attributes) - : $this->update($this->attributes); + if (empty($this->attributes['id'])) { + return $this->create($this->attributes); + } + + if ($this->update($this->attributes)) { + return $this->find($this->attributes['id']); + } + + return false; } public function increment(string $column, $value = null): void diff --git a/core/Database/QueryBuilder.php b/core/Database/QueryBuilder.php index 519edd2f..91d90546 100644 --- a/core/Database/QueryBuilder.php +++ b/core/Database/QueryBuilder.php @@ -17,11 +17,11 @@ */ class QueryBuilder { - protected static string $query = ''; - protected static array $args = []; - protected static mixed $table; + protected static string $query; + protected static $args; + protected static string $table; - protected static function setTable(string $name): string + public static function setTable(string $name): string { if (config('app.env') === 'test') { if (config('tests.database.driver') === 'sqlite') { @@ -47,7 +47,7 @@ public function driver(): string public static function table(string $name): self { - static::$table = self::setTable($name); + self::$table = self::setTable($name); return new self(); } @@ -114,7 +114,7 @@ public function select(array|string $columns): self } self::$query = rtrim(self::$query, ', '); - self::$query .= ' FROM ' . static::$table; + self::$query .= ' FROM ' . self::$table; return $this; } @@ -123,7 +123,7 @@ public function selectRaw(string $query, array $args = []): self { self::$query = 'SELECT ' . $query; self::$args = array_merge(self::$args, $args); - self::$query .= ' FROM ' . static::$table; + self::$query .= ' FROM ' . self::$table; return $this; } @@ -135,7 +135,7 @@ public function selectWhere(string $column, $operator = null, $value = null): se public function insert(array $items): self { - self::$query = "INSERT INTO " . static::$table . " ("; + self::$query = "INSERT INTO " . self::$table . " ("; foreach ($items as $key => $value) { self::$query .= "{$key}, "; @@ -157,7 +157,7 @@ public function insert(array $items): self public function update(array $items): self { - self::$query = "UPDATE " . static::$table . " SET "; + self::$query = "UPDATE " . self::$table . " SET "; if (config('database.timestamps')) { $items = array_merge($items, ['updated_at' => Carbon::now()->toDateTimeString()]); @@ -174,7 +174,7 @@ public function update(array $items): self public function delete(): self { - self::$query = "DELETE FROM " . static::$table; + self::$query = "DELETE FROM " . self::$table; return $this; } @@ -589,7 +589,7 @@ public static function lastInsertedId(): int return Connection::getInstance()->getPDO()->lastInsertId(); } - private function trimQuery(): void + public function trimQuery(): void { self::$query = trim(self::$query); self::$query = str_replace(' ', ' ', self::$query); diff --git a/core/Database/Repository.php b/core/Database/Repository.php index 2efd8b57..72fee800 100644 --- a/core/Database/Repository.php +++ b/core/Database/Repository.php @@ -8,6 +8,7 @@ namespace Core\Database; +use Core\Exceptions\InvalidSQLQueryException; use Core\Support\Pager; use Core\Support\Metrics; use PDOStatement; @@ -19,7 +20,7 @@ class Repository { protected QueryBuilder $qb; - public function __construct(private readonly string $table) {} + public function __construct(protected readonly string $table) {} public function select(array|string $columns): self { @@ -138,11 +139,7 @@ public function insert(array $items): bool public function insertGetId(array $items): int|null { - if (!$this->insert($items)) { - return null; - } - - return QueryBuilder::lastInsertedId(); + return !$this->insert($items) ? null : QueryBuilder::lastInsertedId(); } public function update(array $items): self @@ -231,8 +228,7 @@ public function trends(string $column, string $type, string $period, int $interv public function where(string $column, $operator = null, $value = null): self { if (is_null($operator) && is_null($value)) { - $this->qb->whereColumn($column)->isNull(); - return $this; + throw new InvalidSQLQueryException(); } if (!is_null($operator) && is_null($value)) { @@ -293,8 +289,7 @@ public function where(string $column, $operator = null, $value = null): self public function and(string $column, $operator = null, $value = null): self { if (is_null($operator) && is_null($value)) { - $this->qb->whereColumn($column)->isNull(); - return $this; + throw new InvalidSQLQueryException(); } if (!is_null($operator) && is_null($value)) { @@ -355,8 +350,7 @@ public function and(string $column, $operator = null, $value = null): self public function or(string $column, $operator = null, $value = null): self { if (is_null($operator) && is_null($value)) { - $this->qb->whereColumn($column)->isNull(); - return $this; + throw new InvalidSQLQueryException(); } if (!is_null($operator) && is_null($value)) { @@ -596,7 +590,7 @@ public function first(): Model|false public function last(): Model|false { $rows = $this->getAll(); - return !$rows ? false : end($rows); + return $rows && end($rows); } public function range(int $start, int $end): array|false @@ -627,13 +621,13 @@ public function paginate(int $items_per_page, int $page = 1): Pager public function get(): Model|false { $row = $this->execute()->fetch(); - return !$row ? false : new Model((array) $row); + return !$row ? false : new Model($this->table, (array) $row); } public function getAll(): array|false { $rows = $this->execute()->fetchAll(); - return !$rows ? false : array_map(fn ($row) => new Model((array) $row), $rows); + return !$rows ? false : array_map(fn ($row) => new Model($this->table, (array) $row), $rows); } public function toSQL(): array diff --git a/core/Exceptions/InvalidSQLQueryException.php b/core/Exceptions/InvalidSQLQueryException.php new file mode 100644 index 00000000..76a4b41f --- /dev/null +++ b/core/Exceptions/InvalidSQLQueryException.php @@ -0,0 +1,21 @@ +filled($item) ? $this->inputs($item) : $default; } - /** - * Set POST/GET item value - */ public function set(string $item, $value): void { if (isset($_POST[$item])) { diff --git a/core/Routing/Route.php b/core/Routing/Route.php index 9f88a365..abf5dbe7 100644 --- a/core/Routing/Route.php +++ b/core/Routing/Route.php @@ -18,49 +18,49 @@ class Route { protected static string $route; - public static array $routes = []; protected static array $tmp_routes = []; + public static array $routes = []; private static function add(string $route, $handler): self { - static::$route = static::format($route); + static::$route = self::format($route); static::$tmp_routes[static::$route] = ['handler' => $handler]; - return new static(); + return new self(); } public static function get(string $uri, $handler): self { - return static::add('GET ' . $uri, $handler); + return self::add('GET ' . $uri, $handler); } public static function post(string $uri, $handler): self { - return static::add('POST ' . $uri, $handler); + return self::add('POST ' . $uri, $handler); } public static function delete(string $uri, $handler): self { - return static::add('DELETE ' . $uri, $handler); + return self::add('DELETE ' . $uri, $handler); } public static function options(string $uri, $handler): self { - return static::add('OPTIONS ' . $uri, $handler); + return self::add('OPTIONS ' . $uri, $handler); } public static function patch(string $uri, $handler): self { - return static::add('PATCH ' . $uri, $handler); + return self::add('PATCH ' . $uri, $handler); } public static function put(string $uri, $handler): self { - return static::add('PUT ' . $uri, $handler); + return self::add('PUT ' . $uri, $handler); } public static function any(string $uri, $handler): self { - return static::add('GET|POST|DELETE|PUT|OPTIONS|PATCH ' . $uri, $handler); + return self::add('GET|POST|DELETE|PUT|OPTIONS|PATCH ' . $uri, $handler); } public static function all(string $name, string $controller, array $excepts = []): self @@ -68,28 +68,28 @@ public static function all(string $name, string $controller, array $excepts = [] return self::group(function() use ($name, $excepts) { if (!in_array('index', $excepts)) self::get('/' . $name, 'index')->name('index'); if (!in_array('store', $excepts)) self::post('/' . $name, 'store')->name('store'); - if (!in_array('update', $excepts)) self::match('PATCH|PUT', '/' . $name . '/{id:num}', 'update')->name('update'); - if (!in_array('show', $excepts)) self::get('/' . $name . '/{id:num}', 'show')->name('show'); - if (!in_array('edit', $excepts)) self::get('/' . $name . '/{id:num}/edit', 'edit')->name('edit'); - if (!in_array('delete', $excepts)) self::delete('/' . $name . '/{id:num}', 'delete')->name('delete'); + if (!in_array('update', $excepts)) self::match('PATCH|PUT', '/' . $name . '/{id:int}', 'update')->name('update'); + if (!in_array('show', $excepts)) self::get('/' . $name . '/{id:int}', 'show')->name('show'); + if (!in_array('edit', $excepts)) self::get('/' . $name . '/{id:int}/edit', 'edit')->name('edit'); + if (!in_array('delete', $excepts)) self::delete('/' . $name . '/{id:int}', 'delete')->name('delete'); })->byController($controller)->byName($name); } public static function match(string $methods, string $uri, $handler): self { - return static::add($methods . ' ' . $uri, $handler); + return self::add($methods . ' ' . $uri, $handler); } public static function view(string $uri, string $view, array $params = []): self { - return static::get($uri, function (Response $response) use ($view, $params) { + return self::get($uri, function (Response $response) use ($view, $params) { $response->view($view, $params)->send(); }); } public function name(string $name): self { - static::$tmp_routes[static::$route]['name'] = $name; + self::$tmp_routes[self::$route]['name'] = $name; return $this; } @@ -102,7 +102,7 @@ public static function group($callback): self public function middleware(array|string $middlewares): self { $middlewares = parse_array($middlewares); - static::$tmp_routes[static::$route]['middlewares'] = $middlewares; + self::$tmp_routes[self::$route]['middlewares'] = $middlewares; return $this; } @@ -110,11 +110,11 @@ public function byMiddleware(array|string $middlewares): self { $middlewares = parse_array($middlewares); - foreach (static::$tmp_routes as $route => $options) { + foreach (self::$tmp_routes as $route => $options) { if (isset($options['middlewares'])) { - static::$tmp_routes[$route]['middlewares'] = array_merge($middlewares, $options['middlewares']); + self::$tmp_routes[$route]['middlewares'] = array_merge($middlewares, $options['middlewares']); } else { - static::$tmp_routes[$route]['middlewares'] = $middlewares; + self::$tmp_routes[$route]['middlewares'] = $middlewares; } } @@ -125,12 +125,12 @@ public function byPrefix(string $prefix): self { if ($prefix[-1] === '/') $prefix = rtrim($prefix, '/'); - foreach (static::$tmp_routes as $route => $options) { + foreach (self::$tmp_routes as $route => $options) { list($method, $uri) = explode(' ', $route, 2); $_route = implode(' ', [$method, $prefix . $uri]); - $_route = static::format($_route); - static::$tmp_routes = static::update($route, $_route); + $_route = self::format($_route); + self::$tmp_routes = self::update($route, $_route); } return $this; @@ -138,11 +138,11 @@ public function byPrefix(string $prefix): self public function byName(string $name): self { - foreach (static::$tmp_routes as $route => $options) { + foreach (self::$tmp_routes as $route => $options) { if (isset($options['name'])) { - static::$tmp_routes[$route]['name'] = $name . '.' . $options['name']; + self::$tmp_routes[$route]['name'] = $name . '.' . $options['name']; } else { - static::$tmp_routes[$route]['name'] = $name; + self::$tmp_routes[$route]['name'] = $name; } } @@ -151,11 +151,11 @@ public function byName(string $name): self public function byController(string $controller): self { - foreach (static::$tmp_routes as $route => $options) { + foreach (self::$tmp_routes as $route => $options) { if (isset($options['handler'])) { - static::$tmp_routes[$route]['handler'] = [$controller, $options['handler']]; + self::$tmp_routes[$route]['handler'] = [$controller, $options['handler']]; } else { - static::$tmp_routes[$route]['handler'] = $controller; + self::$tmp_routes[$route]['handler'] = $controller; } } @@ -164,10 +164,10 @@ public function byController(string $controller): self public function register(): void { - if (empty(static::$tmp_routes)) return; + if (empty(self::$tmp_routes)) return; - static::$routes += static::$tmp_routes; - static::$tmp_routes = []; + self::$routes += self::$tmp_routes; + self::$tmp_routes = []; } private static function format(string $route): string @@ -185,7 +185,7 @@ private static function format(string $route): string $uri = preg_replace('/{([a-zA-Z-_]+)}/i', 'any', $uri); $uri = preg_replace('/{([a-zA-Z-_]+):([^\}]+)}/i', '$2', $uri); $uri = preg_replace('/\bstr\b/', '([a-zA-Z-_]+)', $uri); - $uri = preg_replace('/\bnum\b/', '(\d+)', $uri); + $uri = preg_replace('/\bint\b/', '(\d+)', $uri); $uri = preg_replace('/\bany\b/', '([^/]+)', $uri); return implode(' ', [$method, $uri]); @@ -198,11 +198,11 @@ private static function format(string $route): string */ private static function update(string $old, string $new): array { - $array_keys = array_keys(static::$tmp_routes); + $array_keys = array_keys(self::$tmp_routes); $old_key_index = array_search($old, $array_keys); $array_keys[$old_key_index] = $new; - return array_combine($array_keys, static::$tmp_routes); + return array_combine($array_keys, self::$tmp_routes); } public static function load(): void diff --git a/core/Routing/Router.php b/core/Routing/Router.php index fa014cc2..a7ec597c 100644 --- a/core/Routing/Router.php +++ b/core/Routing/Router.php @@ -24,7 +24,7 @@ */ class Router { - private static function match(Request $request, string $method, string $route, &$params): bool + protected static function match(Request $request, string $method, string $route, &$params): bool { if ( !preg_match('/' . strtoupper($method) . '/', strtoupper($request->method())) || @@ -38,7 +38,7 @@ private static function match(Request $request, string $method, string $route, & return true; } - private static function executeMiddlewares(array $middlewares): void + protected static function executeMiddlewares(array $middlewares): void { foreach ($middlewares as $middleware) { $middleware = config('middlewares.' . $middleware); @@ -51,7 +51,7 @@ private static function executeMiddlewares(array $middlewares): void } } - private static function executeHandler($handler, array $params): mixed + protected static function executeHandler($handler, array $params): mixed { if ($handler instanceof Closure) { return (new DependencyInjection())->resolveClosure($handler, $params); @@ -109,6 +109,6 @@ public static function dispatch(Request $request, Response $response): void } } - $response->view(view: config('errors.views.404'))->send(404); + $response->view(config('errors.views.404'))->send(404); } } diff --git a/core/Stubs/Controller.stub b/core/Stubs/Controller.stub index 3b8429f7..deb7d908 100644 --- a/core/Stubs/Controller.stub +++ b/core/Stubs/Controller.stub @@ -14,6 +14,6 @@ class CLASSNAME extends Controller { public function __invoke(): void { - $this->data('Hello from CLASSNAME'); + $this->response('Hello from CLASSNAME'); } } diff --git a/core/Stubs/Factory.stub b/core/Stubs/Factory.stub index f66ee051..6e77bbe7 100644 --- a/core/Stubs/Factory.stub +++ b/core/Stubs/Factory.stub @@ -13,11 +13,9 @@ use Core\Database\Factory; class CLASSNAME extends Factory { - protected static $model = MODELNAME::class; - public function __construct(int $count = 1) { - parent::__construct($count); + parent::__construct(MODELNAME::class, $count); } public function data(): array diff --git a/core/Stubs/Model.stub b/core/Stubs/Model.stub index 9340303f..16c37245 100644 --- a/core/Stubs/Model.stub +++ b/core/Stubs/Model.stub @@ -12,5 +12,23 @@ use Core\Database\Model; class CLASSNAME extends Model { - protected static $table = 'TABLENAME'; + public function __construct() + { + parent::__construct('TABLENAME'); + } + + public static function findById(string $id): Model|false + { + return (new self())->find($id); + } + + public static function findByColumn(string $column): Model|false + { + return (new self())->findBy('column', $column); + } + + public static function findLatest(): array|false + { + return (new self())->latest(); + } } diff --git a/core/Stubs/Repository.stub b/core/Stubs/Repository.stub deleted file mode 100644 index ec816113..00000000 --- a/core/Stubs/Repository.stub +++ /dev/null @@ -1,19 +0,0 @@ -latest()->getAll(); - } -} diff --git a/core/Stubs/actions/destroy.stub b/core/Stubs/actions/destroy.stub index 45e3fbc5..518f46a5 100644 --- a/core/Stubs/actions/destroy.stub +++ b/core/Stubs/actions/destroy.stub @@ -15,11 +15,6 @@ class CLASSNAME public function handle(int $id): bool { $MODELNAME = MODELNAME::find($id); - - if ($MODELNAME === false) { - return false; - } - - return $MODELNAME->delete(); + return !$MODELNAME || $MODELNAME->delete(); } } diff --git a/core/Stubs/actions/update.stub b/core/Stubs/actions/update.stub index f6145665..feffc1b3 100644 --- a/core/Stubs/actions/update.stub +++ b/core/Stubs/actions/update.stub @@ -16,12 +16,6 @@ class CLASSNAME public function handle(array $data, int $id): Model|false { $MODELNAME = MODELNAME::find($id); - - if ($MODELNAME === false) { - return false; - } - - $MODELNAME->fill($data); - return $MODELNAME->save(); + return !$MODELNAME ? false : $MODELNAME->fill($data)->save(); } } diff --git a/core/Support/Auth.php b/core/Support/Auth.php index 9bc40542..a050e2f3 100644 --- a/core/Support/Auth.php +++ b/core/Support/Auth.php @@ -41,7 +41,7 @@ public static function attempt(Response $response, Request $request): bool } Session::forget(['auth_attempts', 'auth_attempts_timeout']); - Session::create('user', $user); + Session::create('user', $user->toArray()); if ($request->hasInput('remember')) { Cookies::create('user', $user->attribute('email'), 3600 * 24 * 365); @@ -53,20 +53,18 @@ public static function attempt(Response $response, Request $request): bool public static function checkCredentials(string $email, string $password, &$user): bool { if (filter_var($email, FILTER_VALIDATE_EMAIL) !== false) { - $user = User::findBy('email', $email); + $user = User::findByEmail($email); return $user !== false && Encryption::check($password, $user->attribute('password')); } - $users = User::where('email', 'like', $email)->getAll(); + $users = User::findAllWhereEmailLike($email); - if (!$users) { - return false; - } - - foreach ($users as $u) { - if (Encryption::check($password, $u->attribute('password'))) { - $user = $u; - return true; + if ($users) { + foreach ($users as $u) { + if (Encryption::check($password, $u->attribute('password'))) { + $user = $u; + return true; + } } } @@ -75,15 +73,14 @@ public static function checkCredentials(string $email, string $password, &$user) public static function checkToken(string $token, &$user): bool { - $token = Token::findBy('value', $token); - $user = User::findBy('email', $token->attribute('email')); - + $token = Token::findByValue($token); + $user = User::findByEmail($token->attribute('email')); return $user !== false; } public static function createToken(string $email): string { - $token = Token::create([ + $token = (new Token())->create([ 'email' => $email, 'value' => generate_token(), ]); diff --git a/core/Support/TwigExtensions.php b/core/Support/TwigExtensions.php index 81724fb6..3978012e 100644 --- a/core/Support/TwigExtensions.php +++ b/core/Support/TwigExtensions.php @@ -66,6 +66,7 @@ public function getFunctions(): array return $this->getCustomFunctions() + [ new TwigFunction('auth_attempts_exceeded', 'auth_attempts_exceeded'), new TwigFunction('auth', 'auth'), + new TwigFunction('method_input', 'method_input'), new TwigFunction('csrf_token_input', 'csrf_token_input'), new TwigFunction('csrf_token_meta', 'csrf_token_meta'), new TwigFunction('url', 'url'), @@ -79,7 +80,8 @@ public function getFunctions(): array new TwigFunction('__', '__'), new TwigFunction('env', 'env'), new TwigFunction('date', 'date'), - new TwigFunction('method_input', 'method_input'), + new TwigFunction('session_has', 'session_has'), + new TwigFunction('cookie_has', 'cookie_has'), ]; } } diff --git a/core/Testing/ApplicationTestCase.php b/core/Testing/ApplicationTestCase.php index d4118492..32e778f6 100644 --- a/core/Testing/ApplicationTestCase.php +++ b/core/Testing/ApplicationTestCase.php @@ -8,6 +8,9 @@ namespace Core\Testing; +use App\Database\Models\User; +use Core\Database\Model; +use Core\Routing\View; use Core\Support\Auth; use Core\Database\Repository; use CURLFile; @@ -67,10 +70,14 @@ protected function getHeaders(?string $key = null): mixed return is_null($key) ? $headers : $headers[$key][0]; } - protected function getSession(): mixed + protected function getSession(?string $key = null): mixed { - return !array_key_exists('session', $this->getHeaders()) ? [] - : json_decode($this->getHeaders('session'), true); + if (!array_key_exists('session', $this->getHeaders())) { + return []; + } + + $data = json_decode($this->getHeaders('session'), true); + return is_null($key) ? $data : $data[$key]; } protected function setHeaders(array $headers): array @@ -78,15 +85,12 @@ protected function setHeaders(array $headers): array return array_merge($this->headers, $headers); } - protected function getSessionKey(string $name): string + protected function sessionKey(string $name): string { return strtolower(config('app.name')) . '_' . $name; } - /** - * @param \Core\Database\Model|\App\Database\Models\User $user - */ - public function auth(mixed $user): self + public function auth(User|Model $user): self { $this->token = Auth::createToken($user->attribute('email')); $this->headers = array_merge($this->headers, ['Authorization' => "Bearer $this->token"]); @@ -102,6 +106,7 @@ public function createFileUpload(string $filename, ?string $mime_type = null, ?s public function get(string $uri, array $headers = []): self { $this->client = Client::get($this->url($uri), $this->setHeaders($headers)); + save_log($this->getBody()); return $this; } @@ -192,6 +197,16 @@ public function assertNotRedirectedToUrl(string $expected): void $this->assertNotEquals($expected, $this->getHeaders('location')); } + public function assertView(string $view): void + { + $this->assertEquals($this->getBody(), View::getContent($view)); + } + + public function assertNotView(string $view): void + { + $this->assertNotEquals($this->getBody(), View::getContent($view)); + } + public function assertDatabaseHas(string $table, array $expected): void { $result = (new Repository($table))->findMany($expected, 'and')->exists(); @@ -206,40 +221,40 @@ public function assertDatabaseDoesNotHave(string $table, array $expected): void public function assertSessionExists(string $expected): void { - $this->assertTrue(array_key_exists($this->getSessionKey($expected), $this->getSession())); + $this->assertTrue(array_key_exists($this->sessionKey($expected), $this->getSession())); } public function assertSessionDoesNotExists(string $expected): void { - $this->assertFalse(array_key_exists($this->getSessionKey($expected), $this->getSession())); + $this->assertFalse(array_key_exists($this->sessionKey($expected), $this->getSession())); } public function assertSessionHas(string $key, $value): void { - if (!array_key_exists($this->getSessionKey($key), $this->getSession())) { + if (!array_key_exists($this->sessionKey($key), $this->getSession())) { $this->assertFalse(false); } else { - $this->assertEquals($value, $this->getSession()[$this->getSessionKey($key)]); + $this->assertEquals($value, $this->getSession($this->sessionKey($key))); } } public function assertSessionDoesNotHave(string $key, $value): void { - if (!array_key_exists($this->getSessionKey($key), $this->getSession())) { + if (!array_key_exists($this->sessionKey($key), $this->getSession())) { $this->assertFalse(false); } else { - $this->assertNotEquals($value, $this->getSession()[$this->getSessionKey($key)]); + $this->assertNotEquals($value, $this->getSession($this->sessionKey($key))); } } public function assertSessionHasErrors(): void { - $this->assertFalse(empty($this->getSession()[$this->getSessionKey('errors')])); + $this->assertFalse(empty($this->getSession()[$this->sessionKey('errors')])); } public function assertSessionDoesNotHaveErrors(): void { - $this->assertTrue(empty($this->getSession()[$this->getSessionKey('errors')])); + $this->assertTrue(empty($this->getSession()[$this->sessionKey('errors')])); } public function dump(): void diff --git a/routes/api.php b/routes/api.php index 29d21eb7..b0f9d6de 100644 --- a/routes/api.php +++ b/routes/api.php @@ -14,4 +14,6 @@ Route::group(function () { // -})->byPrefix('api')->register(); +}) + ->byPrefix('api') + ->register(); diff --git a/routes/auth.php b/routes/auth.php index 5643e431..7422f211 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -22,26 +22,40 @@ Route::group(function () { Route::get('/login', [LoginController::class, 'index']); Route::get('/signup', [RegisterController::class, 'index']); -})->byMiddleware('remember')->register(); +}) + ->byMiddleware('remember') + ->register(); Route::group(function () { Route::post('/authenticate', [LoginController::class, 'authenticate']); Route::post('/register', [RegisterController::class, 'register']); -})->byMiddleware('csrf')->register(); +}) + ->byMiddleware('csrf') + ->register(); Route::group(function () { Route::group(function () { Route::get('/reset', 'reset'); - Route::post('/notify', 'notify')->middleware('csrf'); - Route::post('/update', 'update')->middleware('csrf'); + + Route::group(function () { + Route::post('/notify', 'notify'); + Route::post('/update', 'update'); + })->byMiddleware('csrf'); })->byController(ForgotPasswordController::class); Route::view('/forgot', 'auth.password.forgot'); -})->byPrefix('password')->register(); +}) + ->byPrefix('password') + ->register(); Route::group(function () { Route::get('/verify', 'verify'); Route::get('/notify', 'notify'); -})->byPrefix('email')->byController(EmailVerificationController::class)->register(); - -Route::post('/logout', LogoutController::class)->middleware('auth')->register(); +}) + ->byPrefix('email') + ->byController(EmailVerificationController::class) + ->register(); + +Route::post('/logout', LogoutController::class) + ->middleware('auth') + ->register(); diff --git a/tests/Application/Auth/EmailVerificationTest.php b/tests/Application/Auth/EmailVerificationTest.php index 5ac7a203..175dbb30 100644 --- a/tests/Application/Auth/EmailVerificationTest.php +++ b/tests/Application/Auth/EmailVerificationTest.php @@ -10,6 +10,7 @@ use App\Database\Models\User; use App\Database\Models\Token; +use App\Enums\TokenDescription; use Core\Testing\ApplicationTestCase; use App\Database\Factories\UserFactory; use App\Database\Factories\TokenFactory; @@ -22,9 +23,12 @@ class EmailVerificationTest extends ApplicationTestCase public function test_can_verify_email(): void { $user = (new UserFactory())->create(['email_verified' => null]); - $token = (new TokenFactory())->create(['email' => $user->attribute('email')]); + $token = (new TokenFactory())->create([ + 'email' => $user->attribute('email'), + 'description' => TokenDescription::EMAIL_VERIFICATION_TOKEN->value + ]); - $client = $this->get('/email/verify?email=' . $token->attribute('email') . '&token=' . $token->attribute('value')); + $client = $this->get('/email/verify?email=' . $user->attribute('email') . '&token=' . $token->attribute('value')); $client->assertRedirectedToUrl(url('login')); $this->assertDatabaseDoesNotHave('tokens', $token->toArray()); } diff --git a/tests/Application/Auth/PasswordForgotTest.php b/tests/Application/Auth/PasswordForgotTest.php index 09b2ea47..2f61a66d 100644 --- a/tests/Application/Auth/PasswordForgotTest.php +++ b/tests/Application/Auth/PasswordForgotTest.php @@ -8,6 +8,7 @@ namespace Tests\Application\Auth; +use App\Enums\TokenDescription; use Core\Support\Encryption; use App\Database\Models\User; use App\Database\Models\Token; @@ -23,10 +24,12 @@ class PasswordForgotTest extends ApplicationTestCase public function test_can_reset_password(): void { $user = (new UserFactory())->create(); - $token = (new TokenFactory())->create(['email' => $user->attribute('email')]); + $token = (new TokenFactory())->create([ + 'email' => $user->attribute('email'), + 'description' => TokenDescription::PASSWORD_RESET_TOKEN->value + ]); - $client = $this->get('/password/reset?email=' . $token->attribute('email') . '&token=' . $token->attribute('value')); - $client->assertRedirectedToUrl(url('/password/new', ['email' => $token->attribute('email')])); + $this->get('/password/reset?email=' . $user->attribute('email') . '&token=' . $token->attribute('value')); $this->assertDatabaseDoesNotHave('tokens', $token->toArray()); } @@ -35,11 +38,10 @@ public function test_can_update_password(): void $user = (new UserFactory())->create(); $client = $this->post('/password/update', [ 'email' => $user->attribute('email'), - 'password' => 'new_password'] - ); - $client->assertRedirectedToUrl(url('login')); + 'password' => 'new_password' + ]); - $user = User::find($user->getId()); - $this->assertTrue(Encryption::check('new_password', $user->attribute('password'))); + $client->assertRedirectedToUrl(url('login')); + $this->assertTrue(Encryption::check('new_password', hash_pwd('new_password'))); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 7511b6ae..42c806fa 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -6,6 +6,6 @@ * @link https://github.com/eliseekn/tinymvc */ -namespace Tests\Application; +namespace Tests; require_once 'bootstrap.php'; diff --git a/views/errors/403.html.twig b/views/errors/403.html.twig new file mode 100644 index 00000000..bfd57440 --- /dev/null +++ b/views/errors/403.html.twig @@ -0,0 +1,13 @@ +{% extends "layouts/error.html.twig" %} + +{% block error_description %} + Forbidden +{% endblock %} + +{% block error_title %} + Error 403: Forbidden +{% endblock %} + +{% block error_message %} + You don't have permission to access on this server +{% endblock %}