diff --git a/.env.example b/.env.example index 8c2f682c..31caf0f2 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,12 @@ -APP_ENV=development +APP_ENV=local APP_NAME=TinyMVC APP_URL=http://127.0.0.1:8080/ APP_LANG=en DB_DRIVER=mysql +DB_NAME=tinymvc DB_HOST=127.0.0.1 DB_PORT=3306 -DB_NAME=tinymvc DB_USERNAME= DB_PASSWORD= @@ -16,4 +16,4 @@ MAILER_PORT=1025 MAILER_USERNAME= MAILER_PASSWORD= -ENCRYPTION_KEY=6a6e690053b3f816a6c6b22634e44624beaf28ce2fb25ec9631b1b1fce25 \ No newline at end of file +ENCRYPTION_KEY= \ No newline at end of file diff --git a/Makefile b/Makefile index 96365ddb..e31d3f1e 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ SHELL := /bin/bash -.PHONY: tests +.PHONY: tests, docker tests: php console db:delete php console db:create php console migrations:run - php console tests:run \ No newline at end of file + php console tests:run + +docker: + docker-compose up --build \ No newline at end of file diff --git a/app/Database/Seeds/Seeder.php b/app/Database/Seeds/Seeder.php index 1f225eb4..f150143d 100644 --- a/app/Database/Seeds/Seeder.php +++ b/app/Database/Seeds/Seeder.php @@ -8,6 +8,7 @@ namespace App\Database\Seeds; +use App\Database\Factories\UserFactory; use App\Database\Models\User; /** @@ -17,6 +18,6 @@ class Seeder { public static function run() { - User::factory(5)->create(); + User::factory(UserFactory::class, 5)->create(); } } diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index 077acfdb..753e5126 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -26,7 +26,9 @@ class AuthController { public function login(Request $request, Response $response) { - if (!Auth::check($request)) $response->view('auth.login'); + if (!Auth::check($request)) { + $response->view('auth.login'); + } $uri = !Session::has('intended') ? Auth::HOME : Session::pull('intended'); $response->redirect()->to($uri)->go(); @@ -34,7 +36,9 @@ public function login(Request $request, Response $response) public function signup(Request $request, Response $response) { - if (!Auth::check($request)) $response->view('auth.signup'); + if (!Auth::check($request)) { + $response->view('auth.signup'); + } $uri = !Session::has('intended') ? Auth::HOME : Session::pull('intended'); $response->redirect()->to($uri)->go(); diff --git a/app/Http/Validators/AuthRequest.php b/app/Http/Validators/AuthRequest.php index f03bc385..9c76d256 100644 --- a/app/Http/Validators/AuthRequest.php +++ b/app/Http/Validators/AuthRequest.php @@ -8,9 +8,9 @@ namespace App\Http\Validators; -use Core\Http\Validator\GUMPValidator as Validator; +use Core\Http\Validator\GUMPValidator; -class AuthRequest extends Validator +class AuthRequest extends GUMPValidator { /** * Validation rules diff --git a/app/Http/Validators/RegisterUser.php b/app/Http/Validators/RegisterUser.php index 82017087..0c09fd4f 100644 --- a/app/Http/Validators/RegisterUser.php +++ b/app/Http/Validators/RegisterUser.php @@ -8,10 +8,10 @@ namespace App\Http\Validators; -use Core\Http\Validator\GUMPValidator as Validator; +use Core\Http\Validator\GUMPValidator; use Core\Database\Repository; -class RegisterUser extends Validator +class RegisterUser extends GUMPValidator { /** * Validation rules diff --git a/bootstrap.php b/bootstrap.php index 1dcc603b..a2002782 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -6,23 +6,20 @@ * @link https://github.com/eliseekn/tinymvc */ -use Core\Http\Request; +use Core\Routing\Route; use Core\Support\Config; use Core\Support\Whoops; use Core\Support\Storage; /** - * Define application environnement + * Setup application */ -//application root path define('DS', DIRECTORY_SEPARATOR); define('APP_ROOT', __DIR__ . DS); -//register whoops error handler Whoops::register(); -//setup storages $storage = Storage::path(absolute_path('storage')); if (!$storage->isDir()) $storage->createDir(); @@ -30,7 +27,6 @@ if (!$storage->path(config('storage.cache'))->isDir()) $storage->createDir(); if (!$storage->path(config('storage.sqlite'))->isDir()) $storage->createDir(); -//errors display if (config('errors.display') === true) { ini_set('display_errors', 1); ini_set('error_reporting', E_ALL); @@ -38,7 +34,6 @@ ini_set('display_errors', 0); } -//errors logging if (config('errors.log') === true) { ini_set('log_errors', 1); ini_set('error_log', Storage::path(config('storage.logs'))->file('tinymvc_' . date('m_d_y') . '.log')); @@ -46,21 +41,17 @@ ini_set('log_errors', 0); } -//handle exceptions function handleExceptions($e) { throw new ErrorException($e->getMessage(), $e->getCode(), 1, $e->getFile(), $e->getLine(), $e->getPrevious()); } -//set exceptions and errors handlers set_exception_handler('handleExceptions'); -//remove PHP maximum execution time set_time_limit(0); -//load .env file -if (!Storage::path()->isFile('.env') && !empty((new Request())->uri())) { - throw new Exception('Run "php console app:setup" console command to setup application or copy ".env.example" file to ".env"'); -} +Route::load(); + +if (Storage::path()->isFile('.env')) Config::loadEnv(); -Config::loadEnv(); +throw new Exception('Copy ".env.example" file to ".env" then edit or run "php console app:setup" console command to setup application'); diff --git a/composer.json b/composer.json index 1a488b44..5928e1d2 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,6 @@ "phpunit/phpunit": "^9.5", "symfony/var-dumper": "^5.2", "filp/whoops": "^2.12", - "fakerphp/faker": "^1.15", - "brianium/paratest": "^6.3" + "fakerphp/faker": "^1.15" } } diff --git a/composer.lock b/composer.lock index 8f16369b..a664aba1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "af420ed118b3d703a305e97b6e22244d", + "content-hash": "21d8b44028f4f2fa5b989818bbe930dd", "packages": [ { "name": "dflydev/dot-access-data", @@ -1490,100 +1490,6 @@ } ], "packages-dev": [ - { - "name": "brianium/paratest", - "version": "v6.3.1", - "source": { - "type": "git", - "url": "https://github.com/paratestphp/paratest.git", - "reference": "3d81e35876f6497467310b123583cca6bd4c38f2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/3d81e35876f6497467310b123583cca6bd4c38f2", - "reference": "3d81e35876f6497467310b123583cca6bd4c38f2", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-simplexml": "*", - "php": "^7.3 || ^8.0", - "phpunit/php-code-coverage": "^9.2.6", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-timer": "^5.0.3", - "phpunit/phpunit": "^9.5.8", - "sebastian/environment": "^5.1.3", - "symfony/console": "^4.4.23 || ^5.3.6", - "symfony/process": "^4.4.22 || ^5.3.4" - }, - "require-dev": { - "doctrine/coding-standard": "^9.0.0", - "ekino/phpstan-banned-code": "^0.4.0", - "ergebnis/phpstan-rules": "^0.15.3", - "ext-posix": "*", - "infection/infection": "^0.24", - "phpstan/phpstan": "^0.12.94", - "phpstan/phpstan-deprecation-rules": "^0.12.6", - "phpstan/phpstan-phpunit": "^0.12.21", - "phpstan/phpstan-strict-rules": "^0.12.10", - "squizlabs/php_codesniffer": "^3.6.0", - "symfony/filesystem": "^5.3.4", - "thecodingmachine/phpstan-strict-rules": "^0.12.1", - "vimeo/psalm": "^4.9.2" - }, - "bin": [ - "bin/paratest" - ], - "type": "library", - "autoload": { - "psr-4": { - "ParaTest\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Brian Scaturro", - "email": "scaturrob@gmail.com", - "role": "Developer" - }, - { - "name": "Filippo Tessarotto", - "email": "zoeslam@gmail.com", - "role": "Developer" - } - ], - "description": "Parallel testing for PHP", - "homepage": "https://github.com/paratestphp/paratest", - "keywords": [ - "concurrent", - "parallel", - "phpunit", - "testing" - ], - "support": { - "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v6.3.1" - }, - "funding": [ - { - "url": "https://github.com/sponsors/Slamdunk", - "type": "github" - }, - { - "url": "https://paypal.me/filippotessarotto", - "type": "paypal" - } - ], - "time": "2021-08-10T07:38:58+00:00" - }, { "name": "doctrine/instantiator", "version": "1.4.0", @@ -3873,12 +3779,12 @@ } ], "aliases": [], - "minimum-stability": "stable", + "minimum-stability": "dev", "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.2" + "php": ">=7.2" }, "platform-dev": [], "plugin-api-version": "2.0.0" diff --git a/config/app.php b/config/app.php index 4afb2239..0eeb95eb 100644 --- a/config/app.php +++ b/config/app.php @@ -11,7 +11,7 @@ */ return [ - 'env' => env('APP_ENV', 'test'), + 'env' => env('APP_ENV', 'local'), 'name' => env('APP_NAME', 'TinyMVC'), 'url' => env('APP_URL', 'http://127.0.0.1:8080/'), 'lang' => env('APP_LANG', 'en'), diff --git a/console b/console index 2be6616a..819fe9f0 100644 --- a/console +++ b/console @@ -43,6 +43,7 @@ $console->add(new \Core\Console\Make\Test()); $console->add(new \Core\Console\App\Setup()); $console->add(new \Core\Console\App\EncryptionKey()); +$console->add(new \Core\Console\App\Environnement()); $console->add(new \Core\Console\Cache()); $console->add(new \Core\Console\Server()); diff --git a/core/Application.php b/core/Application.php index 7fbd3c1d..a200e97e 100644 --- a/core/Application.php +++ b/core/Application.php @@ -11,7 +11,6 @@ use Core\Http\Request; use Core\Routing\Router; use Core\Support\Whoops; -use Core\Support\Storage; use Core\Support\Exception; use Core\Http\Response\Response; @@ -20,34 +19,22 @@ */ class Application { - private $response; - private $request; - public function __construct() { - $this->request = new Request(); - $this->response = new Response(); - Whoops::register(); - - $routes = Storage::path(config('storage.routes'))->getFiles(); - - foreach ($routes as $route) { - require_once config('storage.routes') . $route; - } } - public function run() + public function execute() { - try { - Router::dispatch($this->request, $this->response); - } + $response = new Response(); + + try { Router::dispatch(new Request(), $response); } catch (Exception $e) { if (config('errors.log')) save_log('Exception: ' . $e); if (config('errors.display')) die($e); - $this->response->view(config('errors.views.500'), [], 500); + $response->view(config('errors.views.500'), [], 500); } } } diff --git a/core/Console/App/EncryptionKey.php b/core/Console/App/EncryptionKey.php index 06f29ce7..e6a7f116 100644 --- a/core/Console/App/EncryptionKey.php +++ b/core/Console/App/EncryptionKey.php @@ -27,26 +27,26 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - $config = [ - 'APP_ENV' => config('app.env') . PHP_EOL, - 'APP_NAME' => config('app.name') . PHP_EOL, - 'APP_URL' => config('app.url') . PHP_EOL, - 'APP_LANG' => config('app.lang') . PHP_EOL . PHP_EOL, - 'DB_DRIVER' => config('database.driver') . PHP_EOL, - 'DB_NAME' => config('database.name') . PHP_EOL, - 'DB_HOST' => config('database.' . config('database.driver') . '.host') . PHP_EOL, - 'DB_PORT' => config('database.' . config('database.driver') . '.port') . PHP_EOL, - 'DB_USERNAME' => config('database.' . config('database.driver') . '.username') . PHP_EOL, - 'DB_PASSWORD' => config('database.' . config('database.driver') . '.password') . PHP_EOL . PHP_EOL, - 'MAILER_TRANSPORT' => config('mailer.transport') . PHP_EOL, - 'MAILER_HOST' => config('mailer.' . config('mailer.transport') . '.host') . PHP_EOL, - 'MAILER_PORT' => config('mailer.' . config('mailer.transport') . '.port') . PHP_EOL, - 'MAILER_USERNAME' => config('mailer.' . config('mailer.transport') . '.username') . PHP_EOL, - 'MAILER_PASSWORD' => config('mailer.' . config('mailer.transport') . '.password') . PHP_EOL . PHP_EOL, + Config::loadEnv(); + + Config::saveEnv([ + 'APP_ENV' => env('APP_ENV') . PHP_EOL, + 'APP_NAME' => env('APP_NAME') . PHP_EOL, + 'APP_URL' => env('APP_URL') . PHP_EOL, + 'APP_LANG' => env('APP_LANG') . PHP_EOL . PHP_EOL, + 'DB_DRIVER' => env('DB_DRIVER') . PHP_EOL, + 'DB_NAME' => env('DB_NAME') . PHP_EOL, + 'DB_HOST' => env('DB_HOST') . PHP_EOL, + 'DB_PORT' => env('DB_PORT') . PHP_EOL, + 'DB_USERNAME' => env('DB_USERNAME') . PHP_EOL, + 'DB_PASSWORD' => env('DB_PASSWORD') . PHP_EOL . PHP_EOL, + 'MAILER_TRANSPORT' => env('MAILER_TRANSPORT') . PHP_EOL, + 'MAILER_HOST' => env('MAILER_HOST') . PHP_EOL, + 'MAILER_PORT' => env('MAILER_PORT') . PHP_EOL, + 'MAILER_USERNAME' => env('MAILER_USERNAME') . PHP_EOL, + 'MAILER_PASSWORD' => env('MAILER_PASSWORD') . PHP_EOL . PHP_EOL, 'ENCRYPTION_KEY' => generate_token() - ]; - - Config::saveEnv($config); + ]); $output->writeln('Application encryption key has been generated'); diff --git a/core/Console/App/Environnement.php b/core/Console/App/Environnement.php new file mode 100644 index 00000000..cb1c8f8b --- /dev/null +++ b/core/Console/App/Environnement.php @@ -0,0 +1,58 @@ +setDescription('Define application environnement'); + $this->addArgument('env', InputArgument::REQUIRED, 'Specify application environnement (test, local or prod'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + Config::loadEnv(); + + Config::saveEnv([ + 'APP_ENV' => $input->getArgument('env') . PHP_EOL, + 'APP_ENV' => env('APP_ENV') . PHP_EOL, + 'APP_NAME' => env('APP_NAME') . PHP_EOL, + 'APP_URL' => env('APP_URL') . PHP_EOL, + 'APP_LANG' => env('APP_LANG') . PHP_EOL . PHP_EOL, + 'DB_DRIVER' => env('DB_DRIVER') . PHP_EOL, + 'DB_NAME' => env('DB_NAME') . PHP_EOL, + 'DB_HOST' => env('DB_HOST') . PHP_EOL, + 'DB_PORT' => env('DB_PORT') . PHP_EOL, + 'DB_USERNAME' => env('DB_USERNAME') . PHP_EOL, + 'DB_PASSWORD' => env('DB_PASSWORD') . PHP_EOL . PHP_EOL, + 'MAILER_TRANSPORT' => env('MAILER_TRANSPORT') . PHP_EOL, + 'MAILER_HOST' => env('MAILER_HOST') . PHP_EOL, + 'MAILER_PORT' => env('MAILER_PORT') . PHP_EOL, + 'MAILER_USERNAME' => env('MAILER_USERNAME') . PHP_EOL, + 'MAILER_PASSWORD' => env('MAILER_PASSWORD') . PHP_EOL . PHP_EOL, + 'ENCRYPTION_KEY' => env('ENCRYPTION_KEY') + ]); + + $output->writeln('Application environnement has been defined'); + + return Command::SUCCESS; + } +} diff --git a/core/Console/App/Setup.php b/core/Console/App/Setup.php index c1d542eb..b160c003 100644 --- a/core/Console/App/Setup.php +++ b/core/Console/App/Setup.php @@ -35,63 +35,52 @@ protected function execute(InputInterface $input, OutputInterface $output) while (!$finish_setup) { $output->write('Application name (default: TinyMVC): '); - $config['APP_NAME'] = fgets(STDIN); - if (strlen($config['APP_NAME']) <= 1) $config['APP_NAME'] = 'TinyMVC' . PHP_EOL; + $config['APP_NAME'] = $this->getInput('TinyMVC'); $output->write('Application url (default: http://127.0.0.1:8080/): '); - $app_url = fgets(STDIN); - - if (strlen($app_url) <= 1) $app_url = 'http://127.0.0.1:8080/'; - - $app_url = rtrim($app_url, PHP_EOL); + $app_url = trim($this->getInput('http://127.0.0.1:8080/')); - if (!empty($app_url) && $app_url[-1] !== '/') $app_url = $app_url . '/'; + if (!empty($app_url) && $app_url[-1] !== '/') { + $app_url = $app_url . '/'; + } $config['APP_URL'] = $app_url . PHP_EOL; $output->write('Application language (default: en): '); - $config['APP_LANG'] = fgets(STDIN); - if (strlen($config['APP_LANG']) <= 1) $config['APP_LANG'] = 'en' . PHP_EOL . PHP_EOL; + $config['APP_LANG'] = $this->getInput('en') . PHP_EOL; $output->write('Database driver [mysql (default), sqlite]: '); - $config['DB_DRIVER'] = fgets(STDIN); - if (strlen($config['DB_DRIVER']) <= 1 || !in_array($config['DB_DRIVER'], ['mysql', 'sqlite'])) $config['DB_DRIVER'] = 'mysql' . PHP_EOL; + $config['DB_DRIVER'] = $this->getInput('mysql', ['mysql', 'sqlite']); $output->write('Database host (default: 127.0.0.1): '); - $config['DB_HOST'] = fgets(STDIN); - if (strlen($config['DB_HOST']) <= 1) $config['DB_HOST'] = '127.0.0.1' . PHP_EOL; + $config['DB_HOST'] = $this->getInput('127.0.0.1'); $output->write('Database port (default: 3306): '); - $config['DB_PORT'] = fgets(STDIN); - if (strlen($config['DB_PORT']) <= 1) $config['DB_PORT'] = '3306' . PHP_EOL; + $config['DB_PORT'] = $this->getInput('3306'); $output->write('Database name (default: tinymvc): '); - $config['DB_NAME'] = fgets(STDIN); - if (strlen($config['DB_NAME']) <= 1) $config['DB_NAME'] = 'tinymvc' . PHP_EOL; + $config['DB_NAME'] = $this->getInput('tinymvc'); $output->write('Database username: '); - $config['DB_USERNAME'] = fgets(STDIN); + $config['DB_USERNAME'] = $this->getInput(''); $output->write('Database password: '); - $config['DB_PASSWORD'] = fgets(STDIN) . PHP_EOL; + $config['DB_PASSWORD'] = $this->getInput('') . PHP_EOL; $output->write('Mailer transport [smtp (default), sendmail]: '); - $config['MAILER_TRANSPORT'] = fgets(STDIN); - if (strlen($config['MAILER_TRANSPORT']) <= 1 || !in_array($config['MAILER_TRANSPORT'], ['smtp', 'sendmail'])) $config['MAILER_TRANSPORT'] = 'smtp' . PHP_EOL; + $config['MAILER_TRANSPORT'] = $this->getInput('smtp', ['smtp', 'sendmail']); $output->write('Mailer host (default: 127.0.0.1): '); - $config['MAILER_HOST'] = fgets(STDIN); - if (strlen($config['MAILER_HOST']) <= 1) $config['MAILER_HOST'] = '127.0.0.1' . PHP_EOL; + $config['MAILER_HOST'] = $this->getInput('127.0.0.1'); $output->write('Mailer port (default: 1025): '); - $config['MAILER_PORT'] = fgets(STDIN); - if (strlen($config['MAILER_PORT']) <= 1) $config['MAILER_PORT'] = '1025' . PHP_EOL; + $config['MAILER_PORT'] = $this->getInput('1025'); $output->write('Mailer username: '); - $config['MAILER_USERNAME'] = fgets(STDIN); + $config['MAILER_USERNAME'] = $this->getInput(''); $output->write('Mailer password: '); - $config['MAILER_PASSWORD'] = fgets(STDIN) . PHP_EOL; + $config['MAILER_PASSWORD'] = $this->getInput('') . PHP_EOL; $finish_setup = true; } @@ -104,8 +93,25 @@ protected function execute(InputInterface $input, OutputInterface $output) Storage::path(config('storage.lang'))->copyFile('en.php', config('app.lang') . '.php'); } - $output->writeln('Application has been setted up. You need to restart server to make changes applies.'); + $output->writeln('Application has been setted up. You need to restart server to apply changes.'); return Command::SUCCESS; } + + private function getInput(string $default, ?array $expected = null) + { + $input = fgets(STDIN); + + if (is_null($expected)) { + if ($input === "\n") { + return $default . PHP_EOL; + } + } else { + if (!in_array(trim($input), $expected)) { + return $default . PHP_EOL; + } + } + + return $input; + } } diff --git a/core/Console/Make/Make.php b/core/Console/Make/Make.php index d80af0df..3009a100 100644 --- a/core/Console/Make/Make.php +++ b/core/Console/Make/Make.php @@ -52,28 +52,23 @@ public static function generateClass(string $base_name, string $suffix = '', boo { $name = ucfirst(strtolower($base_name)); - if (!$singular) { - $name = self::fixPluralTypo($name); - } - - if ($force_singlular) { - $name = self::fixPluralTypo($name, true); - } + if (!$singular) $name = self::fixPluralTypo($name); + if ($force_singlular) $name = self::fixPluralTypo($name, true); if (strpos($name, '_')) { - list($f, $s) = explode('_', $name); - $name = ucfirst($f) . ucfirst($s); - } + $words = explode('_', $name); + $name = ''; - if ($suffix === 'migration') { - $suffix = 'table'; + foreach ($words as $word) { + $name .= ucfirst($word); + } } + if ($suffix === 'migration') $suffix = 'table'; + $class = $name . ucfirst($suffix); - if (strpos($class, 'Table')) { - $class .= date('_YmdHis'); - } + if (strpos($class, 'Table')) $class .= date('_YmdHis'); return [lcfirst($name), $class]; } diff --git a/core/Console/Tests.php b/core/Console/Tests.php index 9d49a805..53e1487f 100644 --- a/core/Console/Tests.php +++ b/core/Console/Tests.php @@ -35,7 +35,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $server_process->setTimeout(null); $server_process->start(); - $process = new Process(['php', 'vendor/bin/paratest', '-p' . config('testing.process')]); + $process = new Process(['php', 'vendor/bin/phpunit']); $process->setTimeout(null); $process->start(); diff --git a/core/Database/Model.php b/core/Database/Model.php index 3a6681a5..59f7b08d 100644 --- a/core/Database/Model.php +++ b/core/Database/Model.php @@ -34,13 +34,9 @@ public function __construct(string $table, $data = null) * * @return \Core\Database\Factory */ - public static function factory(int $count = 1) + public static function factory(string $factory, int $count = 1) { - foreach (Storage::path(config('storage.factories'))->getFiles() as $file) { - $factory = '\App\Database\Factories\\' . get_file_name($file); - - return new $factory($count); - } + return new $factory($count); } public static function findBy(string $column, $operator = null, $value = null) @@ -235,6 +231,7 @@ public function decrement(string $column, $value = null) { if (is_null($value)) { $this->{$column}--; + return; } $this->{$column} = $this->{$column} - $value; @@ -250,7 +247,7 @@ public function toArray(string ...$attributes) $d = []; foreach ($attributes as $attribute) { - if ($this->has($attribute)) { + if (isset($attribute)) { $d = array_merge($d, [ $attribute => $this->{$attribute} ]); diff --git a/core/Http/Client/ClientInterface.php b/core/Http/Client/ClientInterface.php new file mode 100644 index 00000000..ae8903bd --- /dev/null +++ b/core/Http/Client/ClientInterface.php @@ -0,0 +1,14 @@ + $url) { + foreach ($url as $key => $url) { $curl_array[$key] = curl_init(); $curl = $curl_array[$key]; @@ -115,34 +114,34 @@ function ($curl, $header) use (&$response_headers, $key) { return new self(); } - public static function get($urls, array $headers = []): self + public static function get($url, array $headers = []): self { - return self::send('GET', $urls, [], $headers); + return self::send('GET', $url, [], $headers); } - public static function post($urls, array $data = [], array $headers = [], bool $json = false): self + public static function post($url, array $data = [], array $headers = [], bool $json = false): self { - return self::send('POST', $urls, $data, $headers, $json); + return self::send('POST', $url, $data, $headers, $json); } - public static function put($urls, array $data = [], array $headers = [], bool $json = false): self + public static function put($url, array $data = [], array $headers = [], bool $json = false): self { - return self::send('PUT', $urls, $data, $headers, $json); + return self::send('PUT', $url, $data, $headers, $json); } - public static function delete($urls, array $headers = []): self + public static function delete($url, array $headers = []): self { - return self::send('DELETE', $urls, [], $headers); + return self::send('DELETE', $url, [], $headers); } - public static function options($urls, array $data = [], array $headers = [], bool $json = false): self + public static function options($url, array $data = [], array $headers = [], bool $json = false): self { - return self::send('OPTIONS', $urls, $data, $headers, $json); + return self::send('OPTIONS', $url, $data, $headers, $json); } - public static function patch($urls, array $data = [], array $headers = [], bool $json = false): self + public static function patch($url, array $data = [], array $headers = [], bool $json = false): self { - return self::send('PATCH', $urls, $data, $headers, $json); + return self::send('PATCH', $url, $data, $headers, $json); } public function getHeaders() diff --git a/core/Http/Request.php b/core/Http/Request.php index 0d54b445..da6a7f46 100644 --- a/core/Http/Request.php +++ b/core/Http/Request.php @@ -48,6 +48,8 @@ public function queries(?string $key = null, $default = null) public function inputs(?string $key = null, $default = null) { + $_POST = array_merge($_POST, $this->rawData()); + $input = is_null($key) ? $_POST : ($_POST[$key] ?? ''); return empty($input) || is_null($input) ? $default : $input; } @@ -60,7 +62,10 @@ public function files(?string $key = null, $default = null) public function rawData() { - return file_get_contents('php://input'); + $data = []; + parse_raw_http_request($data); + + return $data; } private function getSingleFile(string $input, array $allowed_extensions = []) diff --git a/core/Http/Validator/GUMPValidator.php b/core/Http/Validator/GUMPValidator.php index 79828f74..18094363 100644 --- a/core/Http/Validator/GUMPValidator.php +++ b/core/Http/Validator/GUMPValidator.php @@ -17,15 +17,12 @@ */ class GUMPValidator implements ValidatorInterface { - protected static $rules = []; - protected static $messages = []; + protected static array $rules = []; + protected static array $messages = []; - /** - * @var bool|array - */ + /** @var bool|array */ protected static $errors; - - protected static $inputs = []; + protected static array $inputs = []; public static function addRule(string $rule, callable $callback, string $error_message) { diff --git a/core/Routing/Route.php b/core/Routing/Route.php index 4664cf1e..cbc39b27 100644 --- a/core/Routing/Route.php +++ b/core/Routing/Route.php @@ -8,6 +8,7 @@ namespace Core\Routing; +use Core\Support\Storage; use Core\Http\Response\Response; /** @@ -108,21 +109,6 @@ public static function groupPrefix(string $prefix, $callback): self return new self(); } - public static function group(array $groups, $callback): self - { - $route = new self(); - - if (array_key_exists('prefix', $groups)) { - $route->groupPrefix($groups['prefix'], $callback); - } - - if (array_key_exists('middlewares', $groups)) { - $route->groupMiddlewares($groups['middlewares'], $callback); - } - - return $route; - } - public function register() { if (empty(static::$tmp_routes)) return; @@ -175,4 +161,13 @@ private static function update(string $old, string $new) return $new_array; } + + public static function load() + { + $routes = Storage::path(config('storage.routes'))->getFiles(); + + foreach ($routes as $route) { + require_once config('storage.routes') . $route; + } + } } diff --git a/core/Routing/Router.php b/core/Routing/Router.php index dbea4beb..6e6daa0d 100644 --- a/core/Routing/Router.php +++ b/core/Routing/Router.php @@ -28,6 +28,8 @@ private static function match(Request $request, string $method, string $route, & ) { return false; } + + array_shift($params); return true; } @@ -88,8 +90,6 @@ public static function dispatch(Request $request, Response $response) $request->method($request_method); if (self::match($request, $method, $route, $params)) { - array_shift($params); - if (!isset($options['handler'])) { throw new Exception("No handler defined for route {$route}"); } diff --git a/core/Support/Auth.php b/core/Support/Auth.php index d1580edb..84801c06 100644 --- a/core/Support/Auth.php +++ b/core/Support/Auth.php @@ -102,13 +102,11 @@ public static function check(Request $request) $result = Session::has('user'); if (!$result) { - if (empty($request->getHttpAuth())) { - return false; - } + if (empty($request->getHttpAuth())) return false; list($method, $token) = $request->getHttpAuth(); - $result = trim($method) === 'Bearer' && self::checkToken(Encryption::decrypt($token), $user); + $result = trim($method) === 'Bearer' && self::checkToken(Encryption::decrypt($token), $user); } return $result; diff --git a/core/Support/Helpers.php b/core/Support/Helpers.php index 7ade9045..a2648b29 100644 --- a/core/Support/Helpers.php +++ b/core/Support/Helpers.php @@ -496,3 +496,62 @@ function class_uses_recursive($class) return array_unique($results); } } + +if (!function_exists('parse_raw_http_request')) { + /** + * Parse raw HTTP request data + * + * Pass in $a_data as an array. This is done by reference to avoid copying + * the data around too much. + * + * Any files found in the request will be added by their field name to the + * $data['files'] array. + * + * @ref: http://www.chlab.ch/blog/archives/webdevelopment/manually-parse-raw-http-data-php + * + * @param array Empty array to fill with data + * @return array Associative array of request data + */ +function parse_raw_http_request(array &$a_data) +{ + // read incoming data + $input = file_get_contents('php://input'); + + // grab multipart boundary from content type header + preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); + + // content type is probably regular form-encoded + if (!count($matches)) { + // we expect regular puts to containt a query string containing data + parse_str(urldecode($input), $a_data); + return $a_data; + } + + $boundary = $matches[1]; + + // split content by boundary and get rid of last -- element + $a_blocks = preg_split("/-+$boundary/", $input); + array_pop($a_blocks); + + // loop data blocks + foreach ($a_blocks as $id => $block) { + if (empty($block)) + continue; + + // you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char + + // parse uploaded files + if (strpos($block, 'application/octet-stream') !== FALSE) { + // match "name", then everything after "stream" (optional) except for prepending newlines + preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); + $a_data['files'][$matches[1]] = $matches[2]; + } + // parse all other fields + else { + // match "name" and optional value in between newline sequences + preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); + $a_data[$matches[1]] = $matches[2]; + } + } +} +} \ No newline at end of file diff --git a/core/Testing/ApplicationTestCase.php b/core/Testing/ApplicationTestCase.php index 2e5496ea..29c68f74 100644 --- a/core/Testing/ApplicationTestCase.php +++ b/core/Testing/ApplicationTestCase.php @@ -8,38 +8,37 @@ namespace Core\Testing; -use Core\Http\Client; -use Core\Database\Repository; use Core\Support\Auth; +use Core\Database\Repository; use PHPUnit\Framework\TestCase; +use Core\Http\Client\Curl as Client; /** * Manage application tests */ class ApplicationTestCase extends TestCase { - /** - * @var \Core\Http\Client - */ - private $client; - - private $headers = []; - private $token = ''; + private Client $client; + private array $headers; + private string $token; protected function setUp(): void { - parent::setUp(); + $uses = array_flip(class_uses_recursive(static::class)); + + if (isset($uses[\Core\Testing\Traits\LoadFaker::class])) { + $this->loadFaker(); + } + } + protected function tearDown(): void + { $uses = array_flip(class_uses_recursive(static::class)); if (isset($uses[\Core\Testing\Traits\RefreshDatabase::class])) { $this->refreshDatabase(); } - if (isset($uses[\Core\Testing\Traits\LoadFaker::class])) { - $this->loadFaker(); - } - $this->token = ''; $this->headers = []; } diff --git a/core/Testing/Traits/LoadFaker.php b/core/Testing/Concerns/LoadFaker.php similarity index 92% rename from core/Testing/Traits/LoadFaker.php rename to core/Testing/Concerns/LoadFaker.php index 56b2db33..1252caf4 100644 --- a/core/Testing/Traits/LoadFaker.php +++ b/core/Testing/Concerns/LoadFaker.php @@ -6,7 +6,7 @@ * @link https://github.com/eliseekn/tinymvc */ -namespace Core\Testing\Traits; +namespace Core\Testing\Concerns; use Faker\Factory; diff --git a/core/Testing/Traits/RefreshDatabase.php b/core/Testing/Concerns/RefreshDatabase.php similarity index 93% rename from core/Testing/Traits/RefreshDatabase.php rename to core/Testing/Concerns/RefreshDatabase.php index f30cd593..57ed3c74 100644 --- a/core/Testing/Traits/RefreshDatabase.php +++ b/core/Testing/Concerns/RefreshDatabase.php @@ -6,7 +6,7 @@ * @link https://github.com/eliseekn/tinymvc */ -namespace Core\Testing\Traits; +namespace Core\Testing\Concerns; use Symfony\Component\Process\Process; diff --git a/index.php b/index.php index 500337a1..23f7c004 100644 --- a/index.php +++ b/index.php @@ -16,4 +16,4 @@ require_once 'bootstrap.php'; $app = new Application(); -$app->run(); +$app->execute(); diff --git a/resources/stubs/Validator.stub b/resources/stubs/Validator.stub index 5d3fa550..2533af0b 100644 --- a/resources/stubs/Validator.stub +++ b/resources/stubs/Validator.stub @@ -8,9 +8,9 @@ namespace App\Http\Validators; -use Core\Http\Validator\GUMPValidator as Validator; +use Core\Http\Validator\GUMPValidator; -class CLASSNAME extends Validator +class CLASSNAME extends GUMPValidator { /** * Validation rules diff --git a/tests/Application/AuthenticationTest.php b/tests/Application/AuthenticationTest.php index 2344be64..3b79f379 100644 --- a/tests/Application/AuthenticationTest.php +++ b/tests/Application/AuthenticationTest.php @@ -6,9 +6,10 @@ * @link https://github.com/eliseekn/tinymvc */ +use App\Database\Factories\UserFactory; use App\Database\Models\User; use Core\Testing\ApplicationTestCase; -use Core\Testing\Traits\RefreshDatabase; +use Core\Testing\Concerns\RefreshDatabase; class AuthenticationTest extends ApplicationTestCase { @@ -16,16 +17,14 @@ class AuthenticationTest extends ApplicationTestCase public function testCanNotAuthenticateWithUnregisteredUserCredentials() { - $user = User::factory()->make(); - - $response = $this->post('authenticate', ['email' => $user->email, 'password' => 'password']); + $response = $this->post('authenticate', ['email' => 'a@b.c', 'password' => 'password']); $response->assertSessionHasErrors(); $response->assertRedirectedToUrl(url('login')); } public function testCanAuthenticateWithRegisteredUserCredentials() { - $user = User::factory()->create(); + $user = User::factory(UserFactory::class)->create(); $response = $this->post('authenticate', ['email' => $user->email, 'password' => 'password']); $response->assertSessionDoesNotHaveErrors(); @@ -34,12 +33,23 @@ public function testCanAuthenticateWithRegisteredUserCredentials() public function testCanRegisterUser() { - $user = User::factory()->make(['password' => 'password']); + $user = User::factory(UserFactory::class)->make(['password' => 'password']); $response = $this->post('register', $user->toArray()); $response->assertSessionDoesNotHaveErrors(); $response->assertRedirectedToUrl(url('login')); - $this->assertDatabaseHas('users', ['email' => $user->email]); + $this->assertDatabaseHas('users', $user->toArray('name', 'email')); + } + + public function testCanNotRegisterSameUserTwice() + { + $user = User::factory(UserFactory::class)->make(['password' => 'password']); + + $this->post('register', $user->toArray()); + + $response = $this->post('register', $user->toArray()); + $response->assertSessionHasErrors(); + //$response->assertRedirectedToUrl(url('login')); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 3b82469e..0a15a102 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -6,7 +6,6 @@ * @link https://github.com/eliseekn/tinymvc */ -//load packages and main configuration for tests require_once 'bootstrap.php'; session_start();