| @@ -0,0 +1,13 @@ | |||||
| APP_ENV= | |||||
| APP_HOST= | |||||
| APP_PORT= | |||||
| DB_CONNECTION=mysql | |||||
| DB_HOST=127.0.0.1 | |||||
| DB_PORT=3306 | |||||
| DB_DATABASE=pro-php-mvc | |||||
| DB_USERNAME=root | |||||
| DB_PASSWORD= | |||||
| EMAIL_TOKEN= | |||||
| EMAIL_FROM_NAME= | |||||
| EMAIL_FROM_EMAIL= | |||||
| @@ -0,0 +1,5 @@ | |||||
| .env | |||||
| database/*.sqlite | |||||
| drivers/* | |||||
| vendor | |||||
| *.cache | |||||
| @@ -0,0 +1,23 @@ | |||||
| FROM php:8.2-fpm-bookworm | |||||
| RUN apt-get update \ | |||||
| && apt-get install -y --no-install-recommends nginx git unzip libsqlite3-dev \ | |||||
| && docker-php-ext-install pdo_mysql pdo_sqlite \ | |||||
| && rm -rf /var/lib/apt/lists/* \ | |||||
| && rm -f /etc/nginx/conf.d/default.conf /etc/nginx/sites-enabled/default | |||||
| COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer | |||||
| WORKDIR /var/www/html | |||||
| COPY . /var/www/html | |||||
| COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf | |||||
| COPY docker/php/app.ini /usr/local/etc/php/conf.d/app.ini | |||||
| COPY docker/start-container.sh /usr/local/bin/start-container | |||||
| RUN chmod +x /usr/local/bin/start-container \ | |||||
| && mkdir -p /run/php | |||||
| EXPOSE 80 | |||||
| CMD ["start-container"] | |||||
| @@ -0,0 +1,16 @@ | |||||
| <?php | |||||
| namespace App\Exceptions; | |||||
| use Framework\Support\ExceptionHandler; | |||||
| use Throwable; | |||||
| class Handler extends ExceptionHandler | |||||
| { | |||||
| public function showThrowable(Throwable $throwable) | |||||
| { | |||||
| // add in some reporting... | |||||
| return parent::showThrowable($throwable); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| <?php | |||||
| namespace App\Http\Controllers\Products; | |||||
| use Framework\Routing\Router; | |||||
| class OrderProductController | |||||
| { | |||||
| public function handle(Router $router) | |||||
| { | |||||
| secure(); | |||||
| // use $data to create a database record... | |||||
| $_SESSION['ordered'] = true; | |||||
| return redirect($router->route('show-home-page')); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,22 @@ | |||||
| <?php | |||||
| namespace App\Http\Controllers\Products; | |||||
| use App\Models\Product; | |||||
| use Framework\Routing\Router; | |||||
| class ShowProductController | |||||
| { | |||||
| public function handle(Router $router) | |||||
| { | |||||
| $parameters = $router->current()->parameters(); | |||||
| $product = Product::find((int) $parameters['product']); | |||||
| return view('products/view', [ | |||||
| 'product' => $product, | |||||
| 'orderAction' => $router->route('order-product', ['product' => $product->id]), | |||||
| 'csrf' => csrf(), | |||||
| ]); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,46 @@ | |||||
| <?php | |||||
| namespace App\Http\Controllers; | |||||
| use App\Models\Product; | |||||
| use Framework\Routing\Router; | |||||
| class ShowHomePageController | |||||
| { | |||||
| public function handle(Router $router) | |||||
| { | |||||
| $cache = app('cache'); | |||||
| $products = Product::all(); | |||||
| $productsWithRoutes = array_map(function ($product) use ($router, $cache) { | |||||
| $key = "route-for-product-{$product->id}"; | |||||
| if (!$cache->has($key)) { | |||||
| $cache->put($key, $router->route('view-product', ['product' => $product->id])); | |||||
| } | |||||
| $product->route = $cache->get($key); | |||||
| return $product; | |||||
| }, $products); | |||||
| // app('queue')->push( | |||||
| // fn($name) => app('logging')->info("Hello {$name}"), | |||||
| // 'Chris', | |||||
| // ); | |||||
| // app('logging')->info('Send a task into the background'); | |||||
| // app('queue')->push( | |||||
| // fn($name) => app('email') | |||||
| // ->to('cgpitt@gmail.com') | |||||
| // ->text("Hello {$name}") | |||||
| // ->send(), | |||||
| // 'Chris', | |||||
| // ); | |||||
| return view('home', [ | |||||
| 'products' => $productsWithRoutes, | |||||
| ]); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,27 @@ | |||||
| <?php | |||||
| namespace App\Http\Controllers\Users; | |||||
| use App\Models\User; | |||||
| use Framework\Routing\Router; | |||||
| class LogInUserController | |||||
| { | |||||
| public function handle(Router $router) | |||||
| { | |||||
| secure(); | |||||
| $data = validate($_POST, [ | |||||
| 'email' => ['required', 'email'], | |||||
| 'password' => ['required', 'min:10'], | |||||
| ], 'login_errors'); | |||||
| $user = User::where('email', $data['email'])->first(); | |||||
| if ($user && password_verify($data['password'], $user->password)) { | |||||
| session()->put('user_id', $user->id); | |||||
| } | |||||
| return redirect($router->route('show-home-page')); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| <?php | |||||
| namespace App\Http\Controllers\Users; | |||||
| use Framework\Routing\Router; | |||||
| class LogOutUserController | |||||
| { | |||||
| public function handle(Router $router) | |||||
| { | |||||
| session()->forget('user_id'); | |||||
| return redirect($router->route('show-home-page')); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,34 @@ | |||||
| <?php | |||||
| namespace App\Http\Controllers\Users; | |||||
| use App\Models\User; | |||||
| use Framework\Routing\Router; | |||||
| class RegisterUserController | |||||
| { | |||||
| public function handle(Router $router) | |||||
| { | |||||
| secure(); | |||||
| $data = validate($_POST, [ | |||||
| 'name' => ['required'], | |||||
| 'email' => ['required', 'email'], | |||||
| 'password' => ['required', 'min:10'], | |||||
| ], 'register_errors'); | |||||
| $user = new User(); | |||||
| $user->name = $data['name']; | |||||
| $user->email = $data['email']; | |||||
| $user->password = password_hash($data['password'], PASSWORD_BCRYPT); | |||||
| $user->save(); | |||||
| session()->put('registered', true); | |||||
| app('queue')->push(function($user) { | |||||
| // send a mail to the user... | |||||
| }, $user); | |||||
| return redirect($router->route('show-home-page')); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| <?php | |||||
| namespace App\Http\Controllers\Users; | |||||
| use Framework\Routing\Router; | |||||
| class ShowRegisterFormController | |||||
| { | |||||
| public function handle(Router $router) | |||||
| { | |||||
| return view('users/register', [ | |||||
| 'registerAction' => $router->route('register-user'), | |||||
| 'logInAction' => $router->route('log-in-user'), | |||||
| 'csrf' => csrf(), | |||||
| ]); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| <?php | |||||
| namespace App\Models; | |||||
| use Framework\Database\Model; | |||||
| class Order extends Model | |||||
| { | |||||
| protected string $table = 'orders'; | |||||
| public function user(): mixed | |||||
| { | |||||
| return $this->belongsTo(User::class, 'user_id'); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,27 @@ | |||||
| <?php | |||||
| namespace App\Models; | |||||
| use Framework\Database\Model; | |||||
| class Product extends Model | |||||
| { | |||||
| protected string $table = 'products'; | |||||
| public function getNameAttribute($value): string | |||||
| { | |||||
| return ucwords($value); | |||||
| } | |||||
| public function setDescriptionAttribute(string $value) | |||||
| { | |||||
| $limit = 50; | |||||
| $ending = '...'; | |||||
| if (mb_strwidth($value, 'UTF-8') <= $limit) { | |||||
| return $value; | |||||
| } | |||||
| return rtrim(mb_strimwidth($value, 0, $limit, '', 'UTF-8')) . $ending; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| <?php | |||||
| namespace App\Models; | |||||
| use Framework\Database\Model; | |||||
| class Profile extends Model | |||||
| { | |||||
| protected string $table = 'profiles'; | |||||
| public function user(): mixed | |||||
| { | |||||
| return $this->belongsTo(User::class, 'user_id'); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,20 @@ | |||||
| <?php | |||||
| namespace App\Models; | |||||
| use Framework\Database\TableName; | |||||
| use Framework\Database\Model; | |||||
| #[TableName('users')] | |||||
| class User extends Model | |||||
| { | |||||
| public function profile(): mixed | |||||
| { | |||||
| return $this->hasOne(Profile::class, 'user_id'); | |||||
| } | |||||
| public function orders(): mixed | |||||
| { | |||||
| return $this->hasMany(Order::class, 'user_id'); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| <?php | |||||
| use Framework\Database\Command\MigrateCommand; | |||||
| use Framework\Support\Command\ServeCommand; | |||||
| use Framework\Queue\Command\WorkCommand; | |||||
| return [ | |||||
| MigrateCommand::class, | |||||
| ServeCommand::class, | |||||
| WorkCommand::class, | |||||
| ]; | |||||
| @@ -0,0 +1,51 @@ | |||||
| <?php | |||||
| use App\Http\Controllers\ShowHomePageController; | |||||
| use App\Http\Controllers\Products\OrderProductController; | |||||
| use App\Http\Controllers\Products\ShowProductController; | |||||
| use App\Http\Controllers\Users\LogInUserController; | |||||
| use App\Http\Controllers\Users\LogOutUserController; | |||||
| use App\Http\Controllers\Users\RegisterUserController; | |||||
| use App\Http\Controllers\Users\ShowRegisterFormController; | |||||
| use Framework\Routing\Router; | |||||
| return function(Router $router) { | |||||
| $router->errorHandler( | |||||
| 404, fn() => 'whoops!' | |||||
| ); | |||||
| $router->add( | |||||
| 'GET', '/', | |||||
| [ShowHomePageController::class, 'handle'], | |||||
| )->name('show-home-page'); | |||||
| $router->add( | |||||
| 'GET', '/products/view/{product}', | |||||
| [ShowProductController::class, 'handle'], | |||||
| )->name('view-product'); | |||||
| $router->add( | |||||
| 'POST', '/products/order/{product}', | |||||
| [OrderProductController::class, 'handle'], | |||||
| )->name('order-product'); | |||||
| $router->add( | |||||
| 'GET', '/register', | |||||
| [ShowRegisterFormController::class, 'handle'], | |||||
| )->name('show-register-form'); | |||||
| $router->add( | |||||
| 'POST', '/register', | |||||
| [RegisterUserController::class, 'handle'], | |||||
| )->name('register-user'); | |||||
| $router->add( | |||||
| 'POST', '/log-in', | |||||
| [LogInUserController::class, 'handle'], | |||||
| )->name('log-in-user'); | |||||
| $router->add( | |||||
| 'GET', '/log-out', | |||||
| [LogOutUserController::class, 'handle'], | |||||
| )->name('log-out-user'); | |||||
| }; | |||||
| @@ -0,0 +1,17 @@ | |||||
| <?php | |||||
| require __DIR__ . '/vendor/autoload.php'; | |||||
| $app = \Framework\App::getInstance(); | |||||
| $app->bind('paths.base', fn() => __DIR__); | |||||
| $app->prepare(); | |||||
| $console = new \Symfony\Component\Console\Application(); | |||||
| $commands = require __DIR__ . '/app/commands.php'; | |||||
| foreach ($commands as $command) { | |||||
| $console->add(new $command); | |||||
| } | |||||
| $console->run(); | |||||
| @@ -0,0 +1,36 @@ | |||||
| { | |||||
| "name": "whoosh/website", | |||||
| "description": "A website that sells Whoosh rockets", | |||||
| "scripts": { | |||||
| "serve": "php command.php serve", | |||||
| "test": "vendor/bin/phpunit" | |||||
| }, | |||||
| "autoload": { | |||||
| "psr-4": { | |||||
| "App\\": "app", | |||||
| "Framework\\": "framework" | |||||
| }, | |||||
| "files": [ | |||||
| "framework/helpers.php" | |||||
| ] | |||||
| }, | |||||
| "config": { | |||||
| "process-timeout": 0 | |||||
| }, | |||||
| "require": { | |||||
| "filp/whoops": "^2.1", | |||||
| "vlucas/phpdotenv": "^5.2", | |||||
| "symfony/console": "^5.1", | |||||
| "league/flysystem": "^2.0", | |||||
| "opis/closure": "^3.6", | |||||
| "monolog/monolog": "^2.2", | |||||
| "swiftmailer/swiftmailer": "^6.2", | |||||
| "wildbit/swiftmailer-postmark": "^3.3" | |||||
| }, | |||||
| "require-dev": { | |||||
| "phpunit/phpunit": "^9.5", | |||||
| "symfony/panther": "^0.9.0", | |||||
| "dbrekelmans/bdi": "^0.3.0", | |||||
| "symfony/process": "^5.2" | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| <?php | |||||
| return [ | |||||
| 'default' => 'memory', | |||||
| 'memory' => [ | |||||
| 'type' => 'memory', | |||||
| 'seconds' => 31536000, | |||||
| ], | |||||
| 'file' => [ | |||||
| 'type' => 'file', | |||||
| 'seconds' => 31536000, | |||||
| ], | |||||
| 'memcache' => [ | |||||
| 'type' => 'memcache', | |||||
| 'host' => '127.0.0.1', | |||||
| 'port' => 11211, | |||||
| 'seconds' => 31536000, | |||||
| ], | |||||
| ]; | |||||
| @@ -0,0 +1,17 @@ | |||||
| <?php | |||||
| return [ | |||||
| 'default' => env('DB_CONNECTION', 'mysql'), | |||||
| 'mysql' => [ | |||||
| 'type' => 'mysql', | |||||
| 'host' => env('DB_HOST', '127.0.0.1'), | |||||
| 'port' => env('DB_PORT', '3306'), | |||||
| 'database' => env('DB_DATABASE', 'pro-php-mvc'), | |||||
| 'username' => env('DB_USERNAME', 'root'), | |||||
| 'password' => env('DB_PASSWORD', ''), | |||||
| ], | |||||
| 'sqlite' => [ | |||||
| 'type' => 'sqlite', | |||||
| 'path' => env('DB_SQLITE_PATH', __DIR__ . '/../database/database.sqlite'), | |||||
| ], | |||||
| ]; | |||||
| @@ -0,0 +1,13 @@ | |||||
| <?php | |||||
| return [ | |||||
| 'default' => 'postmark', | |||||
| 'postmark' => [ | |||||
| 'type' => 'postmark', | |||||
| 'token' => env('EMAIL_TOKEN'), | |||||
| 'from' => [ | |||||
| 'name' => env('EMAIL_FROM_NAME'), | |||||
| 'email' => env('EMAIL_FROM_EMAIL'), | |||||
| ], | |||||
| ] | |||||
| ]; | |||||
| @@ -0,0 +1,24 @@ | |||||
| <?php | |||||
| return [ | |||||
| 'default' => 'local', | |||||
| 'local' => [ | |||||
| 'type' => 'local', | |||||
| 'path' => __DIR__ . '/../storage/app', | |||||
| ], | |||||
| 's3' => [ | |||||
| 'type' => 's3', | |||||
| 'key' => '', | |||||
| 'secret' => '', | |||||
| 'token' => '', | |||||
| 'region' => '', | |||||
| 'bucket' => '', | |||||
| ], | |||||
| 'ftp' => [ | |||||
| 'type' => 'ftp', | |||||
| 'host' => '', | |||||
| 'root' => '', | |||||
| 'username' => '', | |||||
| 'password' => '', | |||||
| ], | |||||
| ]; | |||||
| @@ -0,0 +1,5 @@ | |||||
| <?php | |||||
| return [ | |||||
| 'exceptions' => \App\Exceptions\Handler::class, | |||||
| ]; | |||||
| @@ -0,0 +1,11 @@ | |||||
| <?php | |||||
| return [ | |||||
| 'default' => 'stream', | |||||
| 'stream' => [ | |||||
| 'type' => 'stream', | |||||
| 'path' => __DIR__ . '/../storage/app.log', | |||||
| 'name' => 'App', | |||||
| 'minimum' => \Monolog\Logger::DEBUG, | |||||
| ], | |||||
| ]; | |||||
| @@ -0,0 +1,17 @@ | |||||
| <?php | |||||
| return [ | |||||
| // load config first, so the rest can use it... | |||||
| \Framework\Provider\ConfigProvider::class, | |||||
| \Framework\Provider\CacheProvider::class, | |||||
| \Framework\Provider\DatabaseProvider::class, | |||||
| \Framework\Provider\EmailProvider::class, | |||||
| \Framework\Provider\FilesystemProvider::class, | |||||
| \Framework\Provider\LoggingProvider::class, | |||||
| \Framework\Provider\QueueProvider::class, | |||||
| \Framework\Provider\ResponseProvider::class, | |||||
| \Framework\Provider\SessionProvider::class, | |||||
| \Framework\Provider\ValidationProvider::class, | |||||
| \Framework\Provider\ViewProvider::class, | |||||
| ]; | |||||
| @@ -0,0 +1,10 @@ | |||||
| <?php | |||||
| return [ | |||||
| 'default' => 'database', | |||||
| 'database' => [ | |||||
| 'type' => 'database', | |||||
| 'table' => 'jobs', | |||||
| 'attempts' => 3, | |||||
| ], | |||||
| ]; | |||||
| @@ -0,0 +1,9 @@ | |||||
| <?php | |||||
| return [ | |||||
| 'default' => 'native', | |||||
| 'native' => [ | |||||
| 'type' => 'native', | |||||
| 'prefix' => 'framework_', | |||||
| ], | |||||
| ]; | |||||
| @@ -0,0 +1,18 @@ | |||||
| <?php | |||||
| use Framework\Database\Connection\Connection; | |||||
| class CreateOrdersTable | |||||
| { | |||||
| public function migrate(Connection $connection) | |||||
| { | |||||
| $table = $connection->createTable('orders'); | |||||
| $table->id('id'); | |||||
| $table->int('quantity')->default(1); | |||||
| $table->float('price')->nullable(); | |||||
| $table->bool('is_confirmed')->default(false); | |||||
| $table->dateTime('ordered_at')->default('CURRENT_TIMESTAMP'); | |||||
| $table->text('notes'); | |||||
| $table->execute(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| <?php | |||||
| use Framework\Database\Connection\Connection; | |||||
| class AddDeliveryInstructions | |||||
| { | |||||
| public function migrate(Connection $connection) | |||||
| { | |||||
| $table = $connection->alterTable('orders'); | |||||
| $table->text('delivery_instructions'); | |||||
| $table->execute(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| <?php | |||||
| use Framework\Database\Connection\Connection; | |||||
| class ChangeQuantity | |||||
| { | |||||
| public function migrate(Connection $connection) | |||||
| { | |||||
| $table = $connection->alterTable('orders'); | |||||
| $table->int('quantity')->nullable()->alter(); | |||||
| $table->execute(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| <?php | |||||
| use Framework\Database\Connection\Connection; | |||||
| class DropPrice | |||||
| { | |||||
| public function migrate(Connection $connection) | |||||
| { | |||||
| $table = $connection->alterTable('orders'); | |||||
| $table->dropColumn('price'); | |||||
| $table->execute(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| <?php | |||||
| use Framework\Database\Connection\Connection; | |||||
| class CreateProductsTable | |||||
| { | |||||
| public function migrate(Connection $connection) | |||||
| { | |||||
| $table = $connection->createTable('products'); | |||||
| $table->id('id'); | |||||
| $table->string('name'); | |||||
| $table->text('description'); | |||||
| $table->execute(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,31 @@ | |||||
| <?php | |||||
| use Framework\Database\Connection\Connection; | |||||
| class SeedProducts | |||||
| { | |||||
| public function migrate(Connection $connection) | |||||
| { | |||||
| $products = [ | |||||
| [ | |||||
| 'name' => 'Space Tour', | |||||
| 'description' => 'Take a trip on a rocket ship. Our tours are out of this world. Sign up now for a journey you won't soon forget.', | |||||
| ], | |||||
| [ | |||||
| 'name' => 'Large Rocket', | |||||
| 'description' => 'Need to bring some extra space-baggage? Everyone asking you to bring back a moon rock for them? This is the rocket you want...', | |||||
| ], | |||||
| [ | |||||
| 'name' => 'Small Rocket', | |||||
| 'description' => 'Space exploration is expensive. This rocket comes in under budget and atmosphere.', | |||||
| ], | |||||
| ]; | |||||
| foreach ($products as $product) { | |||||
| $connection | |||||
| ->query() | |||||
| ->from('products') | |||||
| ->insert(['name', 'description'], $product); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| <?php | |||||
| use Framework\Database\Connection\Connection; | |||||
| class CreateUsersTable | |||||
| { | |||||
| public function migrate(Connection $connection) | |||||
| { | |||||
| $table = $connection->createTable('users'); | |||||
| $table->id('id'); | |||||
| $table->string('name'); | |||||
| $table->string('email'); | |||||
| $table->string('password'); | |||||
| $table->execute(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| <?php | |||||
| use Framework\Database\Connection\Connection; | |||||
| class CreateProfilesTable | |||||
| { | |||||
| public function migrate(Connection $connection) | |||||
| { | |||||
| $table = $connection->createTable('profiles'); | |||||
| $table->id('id'); | |||||
| $table->int('user_id'); | |||||
| $table->execute(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| <?php | |||||
| use Framework\Database\Connection\Connection; | |||||
| class AddUserId | |||||
| { | |||||
| public function migrate(Connection $connection) | |||||
| { | |||||
| $table = $connection->alterTable('orders'); | |||||
| $table->int('user_id'); | |||||
| $table->execute(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| <?php | |||||
| use Framework\Database\Connection\Connection; | |||||
| class CreateJobsTable | |||||
| { | |||||
| public function migrate(Connection $connection) | |||||
| { | |||||
| $table = $connection->createTable('jobs'); | |||||
| $table->id('id'); | |||||
| $table->text('closure'); | |||||
| $table->text('params'); | |||||
| $table->int('attempts')->default(0); | |||||
| $table->bool('is_complete')->default(false); | |||||
| $table->execute(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,41 @@ | |||||
| services: | |||||
| app: | |||||
| build: | |||||
| context: . | |||||
| dockerfile: Dockerfile | |||||
| container_name: pro-php-mvc-app | |||||
| restart: unless-stopped | |||||
| depends_on: | |||||
| - db | |||||
| ports: | |||||
| - "8080:80" | |||||
| volumes: | |||||
| - ./:/var/www/html | |||||
| - vendor_data:/var/www/html/vendor | |||||
| environment: | |||||
| APP_ENV: dev | |||||
| APP_HOST: 0.0.0.0 | |||||
| APP_PORT: 80 | |||||
| DB_CONNECTION: mysql | |||||
| DB_HOST: db | |||||
| DB_PORT: 3306 | |||||
| DB_DATABASE: pro-php-mvc | |||||
| DB_USERNAME: root | |||||
| DB_PASSWORD: root | |||||
| COMPOSER_ALLOW_SUPERUSER: 1 | |||||
| db: | |||||
| image: mysql:8.0 | |||||
| container_name: pro-php-mvc-db | |||||
| restart: unless-stopped | |||||
| ports: | |||||
| - "33060:3306" | |||||
| environment: | |||||
| MYSQL_DATABASE: pro-php-mvc | |||||
| MYSQL_ROOT_PASSWORD: root | |||||
| volumes: | |||||
| - mysql_data:/var/lib/mysql | |||||
| volumes: | |||||
| mysql_data: | |||||
| vendor_data: | |||||
| @@ -0,0 +1,23 @@ | |||||
| server { | |||||
| listen 80; | |||||
| server_name _; | |||||
| root /var/www/html/public; | |||||
| index index.php; | |||||
| location / { | |||||
| try_files $uri $uri/ /index.php?$query_string; | |||||
| } | |||||
| location ~ \.php$ { | |||||
| include fastcgi_params; | |||||
| fastcgi_index index.php; | |||||
| fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; | |||||
| fastcgi_param DOCUMENT_ROOT $realpath_root; | |||||
| fastcgi_pass 127.0.0.1:9000; | |||||
| } | |||||
| location ~ /\. { | |||||
| deny all; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,3 @@ | |||||
| error_reporting = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED | |||||
| display_errors = On | |||||
| log_errors = On | |||||
| @@ -0,0 +1,53 @@ | |||||
| #!/bin/sh | |||||
| set -eu | |||||
| cd /var/www/html | |||||
| if [ ! -f .env ] && [ -f .env.example ]; then | |||||
| cp .env.example .env | |||||
| fi | |||||
| if [ ! -f vendor/autoload.php ]; then | |||||
| composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader | |||||
| fi | |||||
| if [ "${DB_CONNECTION:-mysql}" = "mysql" ]; then | |||||
| php -r ' | |||||
| $host = getenv("DB_HOST") ?: "db"; | |||||
| $port = getenv("DB_PORT") ?: "3306"; | |||||
| $database = getenv("DB_DATABASE") ?: "pro-php-mvc"; | |||||
| $username = getenv("DB_USERNAME") ?: "root"; | |||||
| $password = getenv("DB_PASSWORD") ?: ""; | |||||
| for ($attempt = 0; $attempt < 30; $attempt++) { | |||||
| try { | |||||
| new PDO("mysql:host={$host};port={$port};dbname={$database}", $username, $password); | |||||
| exit(0); | |||||
| } catch (Throwable $exception) { | |||||
| fwrite(STDERR, "Waiting for MySQL...\n"); | |||||
| sleep(2); | |||||
| } | |||||
| } | |||||
| fwrite(STDERR, "MySQL did not become ready in time.\n"); | |||||
| exit(1); | |||||
| '; | |||||
| if ! php -r ' | |||||
| $host = getenv("DB_HOST") ?: "db"; | |||||
| $port = getenv("DB_PORT") ?: "3306"; | |||||
| $database = getenv("DB_DATABASE") ?: "pro-php-mvc"; | |||||
| $username = getenv("DB_USERNAME") ?: "root"; | |||||
| $password = getenv("DB_PASSWORD") ?: ""; | |||||
| $pdo = new PDO("mysql:host={$host};port={$port};dbname={$database}", $username, $password); | |||||
| $statement = $pdo->query("SHOW TABLES LIKE '\''migrations'\''"); | |||||
| exit($statement->fetch() ? 0 : 1); | |||||
| '; then | |||||
| php command.php migrate | |||||
| fi | |||||
| fi | |||||
| php-fpm -D | |||||
| exec nginx -g "daemon off;" | |||||
| @@ -0,0 +1,78 @@ | |||||
| <?php | |||||
| namespace Framework; | |||||
| use Dotenv\Dotenv; | |||||
| use Framework\Routing\Router; | |||||
| use Framework\Http\Response; | |||||
| class App extends Container | |||||
| { | |||||
| private static $instance; | |||||
| public static function getInstance() | |||||
| { | |||||
| if (!static::$instance) { | |||||
| static::$instance = new static(); | |||||
| } | |||||
| return static::$instance; | |||||
| } | |||||
| private function __construct() {} | |||||
| private function __clone() {} | |||||
| public function prepare(): static | |||||
| { | |||||
| $basePath = $this->resolve('paths.base'); | |||||
| $this->configure($basePath); | |||||
| $this->bindProviders($basePath); | |||||
| return $this; | |||||
| } | |||||
| public function run(): Response | |||||
| { | |||||
| return $this->dispatch($this->resolve('paths.base')); | |||||
| } | |||||
| private function configure(string $basePath) | |||||
| { | |||||
| $dotenv = Dotenv::createImmutable($basePath); | |||||
| $dotenv->load(); | |||||
| } | |||||
| private function bindProviders(string $basePath) | |||||
| { | |||||
| $providers = require "{$basePath}/config/providers.php"; | |||||
| foreach ($providers as $provider) { | |||||
| $instance = new $provider; | |||||
| if (method_exists($instance, 'bind')) { | |||||
| $instance->bind($this); | |||||
| } | |||||
| } | |||||
| } | |||||
| private function dispatch(string $basePath): Response | |||||
| { | |||||
| if (!$this->has(Router::class)) { | |||||
| $router = new Router(); | |||||
| $routes = require "{$basePath}/app/routes.php"; | |||||
| $routes($router); | |||||
| $this->bind(Router::class, fn() => $router); | |||||
| } | |||||
| $response = $this->resolve(Router::class)->dispatch(); | |||||
| if (!$response instanceof Response) { | |||||
| $response = $this->resolve('response')->content($response); | |||||
| } | |||||
| return $response; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,31 @@ | |||||
| <?php | |||||
| namespace Framework\Cache\Driver; | |||||
| interface Driver | |||||
| { | |||||
| /** | |||||
| * Tell if a value is cached (still) | |||||
| */ | |||||
| public function has(string $key): bool; | |||||
| /** | |||||
| * Get a cached value | |||||
| */ | |||||
| public function get(string $key, mixed $default = null): mixed; | |||||
| /** | |||||
| * Put a value into the cache, for an optional number of seconds | |||||
| */ | |||||
| public function put(string $key, mixed $value, int $seconds = null): static; | |||||
| /** | |||||
| * Remove a single cached value | |||||
| */ | |||||
| public function forget(string $key): static; | |||||
| /** | |||||
| * Remove all cached values | |||||
| */ | |||||
| public function flush(): static; | |||||
| } | |||||
| @@ -0,0 +1,111 @@ | |||||
| <?php | |||||
| namespace Framework\Cache\Driver; | |||||
| use Framework\App; | |||||
| class FileDriver implements Driver | |||||
| { | |||||
| private array $config = []; | |||||
| private array $cached = []; | |||||
| public function __construct(array $config) | |||||
| { | |||||
| $this->config = $config; | |||||
| } | |||||
| public function has(string $key): bool | |||||
| { | |||||
| $data = $this->cached[$key] = $this->read($key); | |||||
| return isset($data['expires']) and $data['expires'] > time(); | |||||
| } | |||||
| private function path(string $key): string | |||||
| { | |||||
| $base = $this->base(); | |||||
| $separator = DIRECTORY_SEPARATOR; | |||||
| $key = sha1($key); | |||||
| return "{$base}{$separator}{$key}.json"; | |||||
| } | |||||
| private function base(): string | |||||
| { | |||||
| $base = App::getInstance()->resolve('paths.base'); | |||||
| $separator = DIRECTORY_SEPARATOR; | |||||
| return "{$base}{$separator}storage{$separator}framework{$separator}cache"; | |||||
| } | |||||
| private function read(string $key) | |||||
| { | |||||
| $path = $this->path($key); | |||||
| if (!is_file($path)) { | |||||
| return []; | |||||
| } | |||||
| return json_decode(file_get_contents($path), true); | |||||
| } | |||||
| public function get(string $key, mixed $default = null): mixed | |||||
| { | |||||
| if ($this->has($key)) { | |||||
| return $this->cached[$key]['value']; | |||||
| } | |||||
| return $default; | |||||
| } | |||||
| public function put(string $key, mixed $value, int $seconds = null): static | |||||
| { | |||||
| if (!is_int($seconds)) { | |||||
| $seconds = (int) $this->config['seconds']; | |||||
| } | |||||
| $data = $this->cached[$key] = [ | |||||
| 'value' => $value, | |||||
| 'expires' => time() + $seconds, | |||||
| ]; | |||||
| return $this->write($key, $data); | |||||
| } | |||||
| private function write(string $key, mixed $value): static | |||||
| { | |||||
| file_put_contents($this->path($key), json_encode($value)); | |||||
| return $this; | |||||
| } | |||||
| public function forget(string $key): static | |||||
| { | |||||
| unset($this->cached[$key]); | |||||
| $path = $this->path($key); | |||||
| if (is_file($path)) { | |||||
| unlink($path); | |||||
| } | |||||
| return $this; | |||||
| } | |||||
| public function flush(): static | |||||
| { | |||||
| $this->cached = []; | |||||
| $base = $this->base(); | |||||
| $separator = DIRECTORY_SEPARATOR; | |||||
| $files = glob("{$base}{$separator}*.json"); | |||||
| foreach ($files as $file){ | |||||
| if (is_file($file)) { | |||||
| unlink($file); | |||||
| } | |||||
| } | |||||
| return $this; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,55 @@ | |||||
| <?php | |||||
| namespace Framework\Cache\Driver; | |||||
| use Memcached; | |||||
| class MemcacheDriver implements Driver | |||||
| { | |||||
| private array $config = []; | |||||
| private Memcached $memcache; | |||||
| public function __construct(array $config) | |||||
| { | |||||
| $this->config = $config; | |||||
| $this->memcache = new Memcached(); | |||||
| $this->memcache->addServer($config['host'], $config['port']); | |||||
| } | |||||
| public function has(string $key): bool | |||||
| { | |||||
| return $this->memcache->get($key) !== false; | |||||
| } | |||||
| public function get(string $key, mixed $default = null): mixed | |||||
| { | |||||
| if ($value = $this->memcache->get($key)) { | |||||
| return $value; | |||||
| } | |||||
| return $default; | |||||
| } | |||||
| public function put(string $key, mixed $value, int $seconds = null): static | |||||
| { | |||||
| if (!is_int($seconds)) { | |||||
| $seconds = (int) $this->config['seconds']; | |||||
| } | |||||
| $this->memcache->set($key, $value, time() + $seconds); | |||||
| return $this; | |||||
| } | |||||
| public function forget(string $key): static | |||||
| { | |||||
| $this->memcache->delete($key); | |||||
| return $this; | |||||
| } | |||||
| public function flush(): static | |||||
| { | |||||
| $this->memcache->flush(); | |||||
| return $this; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,54 @@ | |||||
| <?php | |||||
| namespace Framework\Cache\Driver; | |||||
| class MemoryDriver implements Driver | |||||
| { | |||||
| private array $config = []; | |||||
| private array $cached = []; | |||||
| public function __construct(array $config) | |||||
| { | |||||
| $this->config = $config; | |||||
| } | |||||
| public function has(string $key): bool | |||||
| { | |||||
| return isset($this->cached[$key]) && $this->cached[$key]['expires'] > time(); | |||||
| } | |||||
| public function get(string $key, mixed $default = null): mixed | |||||
| { | |||||
| if ($this->has($key)) { | |||||
| return $this->cached[$key]['value']; | |||||
| } | |||||
| return $default; | |||||
| } | |||||
| public function put(string $key, mixed $value, int $seconds = null): static | |||||
| { | |||||
| if (!is_int($seconds)) { | |||||
| $seconds = (int) $this->config['seconds']; | |||||
| } | |||||
| $this->cached[$key] = [ | |||||
| 'value' => $value, | |||||
| 'expires' => time() + $seconds, | |||||
| ]; | |||||
| return $this; | |||||
| } | |||||
| public function forget(string $key): static | |||||
| { | |||||
| unset($this->cached[$key]); | |||||
| return $this; | |||||
| } | |||||
| public function flush(): static | |||||
| { | |||||
| $this->cached = []; | |||||
| return $this; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| <?php | |||||
| namespace Framework\Cache\Exception; | |||||
| use RuntimeException; | |||||
| class DriverException extends RuntimeException | |||||
| { | |||||
| } | |||||
| @@ -0,0 +1,34 @@ | |||||
| <?php | |||||
| namespace Framework\Cache; | |||||
| use Closure; | |||||
| use Framework\Cache\Driver\Driver; | |||||
| use Framework\Cache\Exception\DriverException; | |||||
| use Framework\Support\DriverFactory; | |||||
| class Factory implements DriverFactory | |||||
| { | |||||
| protected array $drivers; | |||||
| public function addDriver(string $alias, Closure $driver): static | |||||
| { | |||||
| $this->drivers[$alias] = $driver; | |||||
| return $this; | |||||
| } | |||||
| public function connect(array $config): Driver | |||||
| { | |||||
| if (!isset($config['type'])) { | |||||
| throw new DriverException('type is not defined'); | |||||
| } | |||||
| $type = $config['type']; | |||||
| if (isset($this->drivers[$type])) { | |||||
| return $this->drivers[$type]($config); | |||||
| } | |||||
| throw new DriverException('unrecognised type'); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,80 @@ | |||||
| <?php | |||||
| namespace Framework; | |||||
| use InvalidArgumentException; | |||||
| use ReflectionFunction; | |||||
| use ReflectionMethod; | |||||
| use ReflectionNamedType; | |||||
| class Container | |||||
| { | |||||
| private array $bindings = []; | |||||
| private array $resolved = []; | |||||
| public function bind(string $alias, callable $factory): static | |||||
| { | |||||
| $this->bindings[$alias] = $factory; | |||||
| $this->resolved[$alias] = null; | |||||
| return $this; | |||||
| } | |||||
| public function resolve(string $alias): mixed | |||||
| { | |||||
| if (!isset($this->bindings[$alias])) { | |||||
| throw new InvalidArgumentException("{$alias} is not bound"); | |||||
| } | |||||
| if (!isset($this->resolved[$alias])) { | |||||
| $this->resolved[$alias] = call_user_func($this->bindings[$alias], $this); | |||||
| } | |||||
| return $this->resolved[$alias]; | |||||
| } | |||||
| public function has(string $alias): bool | |||||
| { | |||||
| return isset($this->bindings[$alias]); | |||||
| } | |||||
| public function call(array|callable $callable, array $parameters = []): mixed | |||||
| { | |||||
| $reflector = $this->getReflector($callable); | |||||
| $dependencies = []; | |||||
| foreach ($reflector->getParameters() as $parameter) { | |||||
| $name = $parameter->getName(); | |||||
| $type = $parameter->getType(); | |||||
| if (isset($parameters[$name])) { | |||||
| $dependencies[$name] = $parameters[$name]; | |||||
| continue; | |||||
| } | |||||
| if ($parameter->isDefaultValueAvailable()) { | |||||
| $dependencies[$name] = $parameter->getDefaultValue(); | |||||
| continue; | |||||
| } | |||||
| if ($type instanceof ReflectionNamedType) { | |||||
| $dependencies[$name] = $this->resolve($type); | |||||
| continue; | |||||
| } | |||||
| throw new InvalidArgumentException("{$name} cannot be resolved"); | |||||
| } | |||||
| return call_user_func($callable, ...array_values($dependencies)); | |||||
| } | |||||
| private function getReflector(array|callable $callable): ReflectionMethod|ReflectionFunction | |||||
| { | |||||
| if (is_array($callable)) { | |||||
| return new ReflectionMethod($callable[0], $callable[1]); | |||||
| } | |||||
| return new ReflectionFunction($callable); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,100 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Command; | |||||
| use Framework\Database\Factory; | |||||
| use Framework\Database\Connection\Connection; | |||||
| use Framework\Database\Connection\MysqlConnection; | |||||
| use Framework\Database\Connection\SqliteConnection; | |||||
| use Symfony\Component\Console\Command\Command; | |||||
| use Symfony\Component\Console\Input\InputArgument; | |||||
| use Symfony\Component\Console\Input\InputInterface; | |||||
| use Symfony\Component\Console\Input\InputOption; | |||||
| use Symfony\Component\Console\Output\OutputInterface; | |||||
| class MigrateCommand extends Command | |||||
| { | |||||
| protected static $defaultName = 'migrate'; | |||||
| protected function configure() | |||||
| { | |||||
| $this | |||||
| ->setDescription('Migrates the database') | |||||
| ->addOption('fresh', null, InputOption::VALUE_NONE, 'Delete all tables before running the migrations') | |||||
| ->setHelp('This command looks for all migration files and runs them'); | |||||
| } | |||||
| protected function execute(InputInterface $input, OutputInterface $output) | |||||
| { | |||||
| $current = getcwd(); | |||||
| $pattern = 'database/migrations/*.php'; | |||||
| $paths = glob("{$current}/{$pattern}"); | |||||
| if (count($paths) < 1) { | |||||
| $this->writeln('No migrations found'); | |||||
| return Command::SUCCESS; | |||||
| } | |||||
| // $connection = $this->connection(); | |||||
| $connection = app('database'); | |||||
| if ($input->getOption('fresh')) { | |||||
| $output->writeln('Dropping existing database tables'); | |||||
| $connection->dropTables(); | |||||
| // $connection = $this->connection(); | |||||
| $connection = app('database'); | |||||
| } | |||||
| if (!$connection->hasTable('migrations')) { | |||||
| $output->writeln('Creating migrations table'); | |||||
| $this->createMigrationsTable($connection); | |||||
| } | |||||
| foreach ($paths as $path) { | |||||
| [$prefix, $file] = explode('_', $path); | |||||
| [$class, $extension] = explode('.', $file); | |||||
| require $path; | |||||
| $output->writeln("Migrating: {$class}"); | |||||
| $obj = new $class(); | |||||
| $obj->migrate($connection); | |||||
| $connection | |||||
| ->query() | |||||
| ->from('migrations') | |||||
| ->insert(['name'], ['name' => $class]); | |||||
| } | |||||
| return Command::SUCCESS; | |||||
| } | |||||
| // private function connection(): Connection | |||||
| // { | |||||
| // $factory = new Factory(); | |||||
| // $factory->addConnector('mysql', function($config) { | |||||
| // return new MysqlConnection($config); | |||||
| // }); | |||||
| // $factory->addConnector('sqlite', function($config) { | |||||
| // return new SqliteConnection($config); | |||||
| // }); | |||||
| // $config = require getcwd() . '/config/database.php'; | |||||
| // return $factory->connect($config[$config['default']]); | |||||
| // } | |||||
| private function createMigrationsTable(Connection $connection) | |||||
| { | |||||
| $table = $connection->createTable('migrations'); | |||||
| $table->id('id'); | |||||
| $table->string('name'); | |||||
| $table->execute(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,45 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Connection; | |||||
| use Framework\Database\Migration\Migration; | |||||
| use Framework\Database\QueryBuilder\QueryBuilder; | |||||
| use Pdo; | |||||
| abstract class Connection | |||||
| { | |||||
| /** | |||||
| * Get the underlying Pdo instance for this connection | |||||
| */ | |||||
| abstract public function pdo(): Pdo; | |||||
| /** | |||||
| * Start a new query on this connection | |||||
| */ | |||||
| abstract public function query(): QueryBuilder; | |||||
| /** | |||||
| * Start a new migration to add a table on this connection | |||||
| */ | |||||
| abstract public function createTable(string $table): Migration; | |||||
| /** | |||||
| * Start a new migration to add a table on this connection | |||||
| */ | |||||
| abstract public function alterTable(string $table): Migration; | |||||
| /** | |||||
| * Return a list of table names on this connection | |||||
| */ | |||||
| abstract public function getTables(): array; | |||||
| /** | |||||
| * Find out if a table exists on this connection | |||||
| */ | |||||
| abstract public function hasTable(string $name): bool; | |||||
| /** | |||||
| * Drop all tables in the current database | |||||
| */ | |||||
| abstract public function dropTables(): int; | |||||
| } | |||||
| @@ -0,0 +1,94 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Connection; | |||||
| use Framework\Database\Migration\MysqlMigration; | |||||
| use Framework\Database\QueryBuilder\MysqlQueryBuilder; | |||||
| use InvalidArgumentException; | |||||
| use Pdo; | |||||
| class MysqlConnection extends Connection | |||||
| { | |||||
| private Pdo $pdo; | |||||
| private string $database; | |||||
| public function __construct(array $config) | |||||
| { | |||||
| [ | |||||
| 'host' => $host, | |||||
| 'port' => $port, | |||||
| 'database' => $database, | |||||
| 'username' => $username, | |||||
| 'password' => $password, | |||||
| ] = $config; | |||||
| if (empty($host) || empty($database) || empty($username)) { | |||||
| throw new InvalidArgumentException('Connection incorrectly configured'); | |||||
| } | |||||
| $this->database = $database; | |||||
| $this->pdo = new Pdo("mysql:host={$host};port={$port};dbname={$database}", $username, $password); | |||||
| } | |||||
| public function pdo(): Pdo | |||||
| { | |||||
| return $this->pdo; | |||||
| } | |||||
| public function query(): MysqlQueryBuilder | |||||
| { | |||||
| return new MysqlQueryBuilder($this); | |||||
| } | |||||
| public function createTable(string $table): MysqlMigration | |||||
| { | |||||
| return new MysqlMigration($this, $table, 'create'); | |||||
| } | |||||
| public function alterTable(string $table): MysqlMigration | |||||
| { | |||||
| return new MysqlMigration($this, $table, 'alter'); | |||||
| } | |||||
| public function getTables(): array | |||||
| { | |||||
| $statement = $this->pdo->prepare('SHOW TABLES'); | |||||
| $statement->execute(); | |||||
| $results = $statement->fetchAll(PDO::FETCH_NUM); | |||||
| $results = array_map(fn($result) => $result[0], $results); | |||||
| return $results; | |||||
| } | |||||
| public function hasTable(string $name): bool | |||||
| { | |||||
| $tables = $this->getTables(); | |||||
| return in_array($name, $tables); | |||||
| } | |||||
| public function dropTables(): int | |||||
| { | |||||
| $statement = $this->pdo->prepare(" | |||||
| SELECT CONCAT('DROP TABLE IF EXISTS `', table_name, '`') | |||||
| FROM information_schema.tables | |||||
| WHERE table_schema = '{$this->database}'; | |||||
| "); | |||||
| $statement->execute(); | |||||
| $dropTableClauses = $statement->fetchAll(PDO::FETCH_NUM); | |||||
| $dropTableClauses = array_map(fn($result) => $result[0], $dropTableClauses); | |||||
| $clauses = [ | |||||
| 'SET FOREIGN_KEY_CHECKS = 0', | |||||
| ...$dropTableClauses, | |||||
| 'SET FOREIGN_KEY_CHECKS = 1', | |||||
| ]; | |||||
| $statement = $this->pdo->prepare(join(';', $clauses) . ';'); | |||||
| return $statement->execute(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,69 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Connection; | |||||
| use Framework\Database\Migration\SqliteMigration; | |||||
| use Framework\Database\QueryBuilder\SqliteQueryBuilder; | |||||
| use InvalidArgumentException; | |||||
| use Pdo; | |||||
| class SqliteConnection extends Connection | |||||
| { | |||||
| private Pdo $pdo; | |||||
| private array $config; | |||||
| public function __construct(array $config) | |||||
| { | |||||
| ['path' => $path] = $config; | |||||
| if (empty($path)) { | |||||
| throw new InvalidArgumentException('Connection incorrectly configured'); | |||||
| } | |||||
| $this->pdo = new Pdo("sqlite:{$path}"); | |||||
| $this->config = $config; | |||||
| } | |||||
| public function pdo(): Pdo | |||||
| { | |||||
| return $this->pdo; | |||||
| } | |||||
| public function query(): SqliteQueryBuilder | |||||
| { | |||||
| return new SqliteQueryBuilder($this); | |||||
| } | |||||
| public function createTable(string $table): SqliteMigration | |||||
| { | |||||
| return new SqliteMigration($this, $table, 'create'); | |||||
| } | |||||
| public function alterTable(string $table): SqliteMigration | |||||
| { | |||||
| return new SqliteMigration($this, $table, 'alter'); | |||||
| } | |||||
| public function getTables(): array | |||||
| { | |||||
| $statement = $this->pdo->prepare("SELECT name FROM sqlite_master WHERE type = 'table'"); | |||||
| $statement->execute(); | |||||
| $results = $statement->fetchAll(PDO::FETCH_NUM); | |||||
| $results = array_map(fn($result) => $result[0], $results); | |||||
| return $results; | |||||
| } | |||||
| public function hasTable(string $name): bool | |||||
| { | |||||
| $tables = $this->getTables(); | |||||
| return in_array($name, $tables); | |||||
| } | |||||
| public function dropTables(): int | |||||
| { | |||||
| file_put_contents($this->config['path'], ''); | |||||
| return 1; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Exception; | |||||
| use PDOException; | |||||
| class ConnectionException extends PDOException | |||||
| { | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Exception; | |||||
| use PDOException; | |||||
| class MigrationException extends PDOException | |||||
| { | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Exception; | |||||
| use PDOException; | |||||
| class ConnectionException extends PDOException | |||||
| { | |||||
| } | |||||
| @@ -0,0 +1,34 @@ | |||||
| <?php | |||||
| namespace Framework\Database; | |||||
| use Closure; | |||||
| use Framework\Database\Connection\Connection; | |||||
| use Framework\Database\Exception\ConnectionException; | |||||
| use Framework\Support\DriverFactory; | |||||
| class Factory implements DriverFactory | |||||
| { | |||||
| protected array $drivers; | |||||
| public function addDriver(string $alias, Closure $driver): static | |||||
| { | |||||
| $this->drivers[$alias] = $driver; | |||||
| return $this; | |||||
| } | |||||
| public function connect(array $config): Connection | |||||
| { | |||||
| if (!isset($config['type'])) { | |||||
| throw new ConnectionException('type is not defined'); | |||||
| } | |||||
| $type = $config['type']; | |||||
| if (isset($this->drivers[$type])) { | |||||
| return $this->drivers[$type]($config); | |||||
| } | |||||
| throw new ConnectionException('unrecognised type'); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Migration\Field; | |||||
| class BoolField extends Field | |||||
| { | |||||
| public ?bool $default = null; | |||||
| public function default(bool $value): static | |||||
| { | |||||
| $this->default = $value; | |||||
| return $this; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Migration\Field; | |||||
| class DateTimeField extends Field | |||||
| { | |||||
| public ?string $default = null; | |||||
| public function default(string $value): static | |||||
| { | |||||
| $this->default = $value; | |||||
| return $this; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,27 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Migration\Field; | |||||
| abstract class Field | |||||
| { | |||||
| public string $name; | |||||
| public bool $nullable = false; | |||||
| public bool $alter = false; | |||||
| public function __construct(string $name) | |||||
| { | |||||
| $this->name = $name; | |||||
| } | |||||
| public function nullable(): static | |||||
| { | |||||
| $this->nullable = true; | |||||
| return $this; | |||||
| } | |||||
| public function alter(): static | |||||
| { | |||||
| $this->alter = true; | |||||
| return $this; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Migration\Field; | |||||
| class FloatField extends Field | |||||
| { | |||||
| public ?float $default = null; | |||||
| public function default(float $value): static | |||||
| { | |||||
| $this->default = $value; | |||||
| return $this; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Migration\Field; | |||||
| use Framework\Database\Exception\MigrationException; | |||||
| class IdField extends Field | |||||
| { | |||||
| public function default() | |||||
| { | |||||
| throw new MigrationException('ID fields cannot have a default value'); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Migration\Field; | |||||
| class IntField extends Field | |||||
| { | |||||
| public ?int $default = null; | |||||
| public function default(int $value): static | |||||
| { | |||||
| $this->default = $value; | |||||
| return $this; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Migration\Field; | |||||
| class StringField extends Field | |||||
| { | |||||
| public ?string $default = null; | |||||
| public function default(string $value): static | |||||
| { | |||||
| $this->default = $value; | |||||
| return $this; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Migration\Field; | |||||
| use Framework\Database\Exception\MigrationException; | |||||
| class TextField extends Field | |||||
| { | |||||
| public ?string $default = null; | |||||
| public function nullable(): static | |||||
| { | |||||
| throw new MigrationException('Text fields cannot be nullable'); | |||||
| } | |||||
| public function default(string $value): static | |||||
| { | |||||
| $this->default = $value; | |||||
| return $this; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,62 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Migration; | |||||
| use Framework\Database\Connection\Connection; | |||||
| use Framework\Database\Migration\Field\BoolField; | |||||
| use Framework\Database\Migration\Field\DateTimeField; | |||||
| use Framework\Database\Migration\Field\FloatField; | |||||
| use Framework\Database\Migration\Field\IdField; | |||||
| use Framework\Database\Migration\Field\IntField; | |||||
| use Framework\Database\Migration\Field\StringField; | |||||
| use Framework\Database\Migration\Field\TextField; | |||||
| abstract class Migration | |||||
| { | |||||
| protected array $fields = []; | |||||
| public function bool(string $name): BoolField | |||||
| { | |||||
| $field = $this->fields[] = new BoolField($name); | |||||
| return $field; | |||||
| } | |||||
| public function dateTime(string $name): DateTimeField | |||||
| { | |||||
| $field = $this->fields[] = new DateTimeField($name); | |||||
| return $field; | |||||
| } | |||||
| public function float(string $name): FloatField | |||||
| { | |||||
| $field = $this->fields[] = new FloatField($name); | |||||
| return $field; | |||||
| } | |||||
| public function id(string $name): IdField | |||||
| { | |||||
| $field = $this->fields[] = new IdField($name); | |||||
| return $field; | |||||
| } | |||||
| public function int(string $name): IntField | |||||
| { | |||||
| $field = $this->fields[] = new IntField($name); | |||||
| return $field; | |||||
| } | |||||
| public function string(string $name): StringField | |||||
| { | |||||
| $field = $this->fields[] = new StringField($name); | |||||
| return $field; | |||||
| } | |||||
| public function text(string $name): TextField | |||||
| { | |||||
| $field = $this->fields[] = new TextField($name); | |||||
| return $field; | |||||
| } | |||||
| abstract public function execute(); | |||||
| abstract public function dropColumn(string $name): static; | |||||
| } | |||||
| @@ -0,0 +1,164 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Migration; | |||||
| use Framework\Database\Connection\MysqlConnection; | |||||
| use Framework\Database\Exception\MigrationException; | |||||
| use Framework\Database\Migration\Field\Field; | |||||
| use Framework\Database\Migration\Field\BoolField; | |||||
| use Framework\Database\Migration\Field\DateTimeField; | |||||
| use Framework\Database\Migration\Field\FloatField; | |||||
| use Framework\Database\Migration\Field\IdField; | |||||
| use Framework\Database\Migration\Field\IntField; | |||||
| use Framework\Database\Migration\Field\StringField; | |||||
| use Framework\Database\Migration\Field\TextField; | |||||
| class MysqlMigration extends Migration | |||||
| { | |||||
| protected MysqlConnection $connection; | |||||
| protected string $table; | |||||
| protected string $type; | |||||
| protected array $drops = []; | |||||
| public function __construct(MysqlConnection $connection, string $table, string $type) | |||||
| { | |||||
| $this->connection = $connection; | |||||
| $this->table = $table; | |||||
| $this->type = $type; | |||||
| } | |||||
| public function execute() | |||||
| { | |||||
| $fields = array_map(fn($field) => $this->stringForField($field), $this->fields); | |||||
| $primary = array_filter($this->fields, fn($field) => $field instanceof IdField); | |||||
| $primaryKey = isset($primary[0]) ? "PRIMARY KEY (`{$primary[0]->name}`)" : ''; | |||||
| if ($this->type === 'create') { | |||||
| $fields = join(PHP_EOL, array_map(fn($field) => "{$field},", $fields)); | |||||
| $query = " | |||||
| CREATE TABLE `{$this->table}` ( | |||||
| {$fields} | |||||
| {$primaryKey} | |||||
| ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; | |||||
| "; | |||||
| } | |||||
| if ($this->type === 'alter') { | |||||
| $fields = join(PHP_EOL, array_map(fn($field) => "{$field};", $fields)); | |||||
| $drops = join(PHP_EOL, array_map(fn($drop) => "DROP COLUMN `{$drop}`;", $this->drops)); | |||||
| $query = " | |||||
| ALTER TABLE `{$this->table}` | |||||
| {$fields} | |||||
| {$drops} | |||||
| "; | |||||
| } | |||||
| $statement = $this->connection->pdo()->prepare($query); | |||||
| $statement->execute(); | |||||
| } | |||||
| private function stringForField(Field $field): string | |||||
| { | |||||
| $prefix = ''; | |||||
| if ($this->type === 'alter') { | |||||
| $prefix = 'ADD'; | |||||
| } | |||||
| if ($field->alter) { | |||||
| $prefix = 'MODIFY'; | |||||
| } | |||||
| if ($field instanceof BoolField) { | |||||
| $template = "{$prefix} `{$field->name}` tinyint(4)"; | |||||
| if ($field->nullable) { | |||||
| $template .= " DEFAULT NULL"; | |||||
| } | |||||
| if ($field->default !== null) { | |||||
| $default = (int) $field->default; | |||||
| $template .= " DEFAULT {$default}"; | |||||
| } | |||||
| return $template; | |||||
| } | |||||
| if ($field instanceof DateTimeField) { | |||||
| $template = "{$prefix} `{$field->name}` datetime"; | |||||
| if ($field->nullable) { | |||||
| $template .= " DEFAULT NULL"; | |||||
| } | |||||
| if ($field->default === 'CURRENT_TIMESTAMP') { | |||||
| $template .= " DEFAULT CURRENT_TIMESTAMP"; | |||||
| } else if ($field->default !== null) { | |||||
| $template .= " DEFAULT '{$field->default}'"; | |||||
| } | |||||
| return $template; | |||||
| } | |||||
| if ($field instanceof FloatField) { | |||||
| $template = "{$prefix} `{$field->name}` float"; | |||||
| if ($field->nullable) { | |||||
| $template .= " DEFAULT NULL"; | |||||
| } | |||||
| if ($field->default !== null) { | |||||
| $template .= " DEFAULT '{$field->default}'"; | |||||
| } | |||||
| return $template; | |||||
| } | |||||
| if ($field instanceof IdField) { | |||||
| return "{$prefix} `{$field->name}` int(11) unsigned NOT NULL AUTO_INCREMENT"; | |||||
| } | |||||
| if ($field instanceof IntField) { | |||||
| $template = "{$prefix} `{$field->name}` int(11)"; | |||||
| if ($field->nullable) { | |||||
| $template .= " DEFAULT NULL"; | |||||
| } | |||||
| if ($field->default !== null) { | |||||
| $template .= " DEFAULT '{$field->default}'"; | |||||
| } | |||||
| return $template; | |||||
| } | |||||
| if ($field instanceof StringField) { | |||||
| $template = "{$prefix} `{$field->name}` varchar(255)"; | |||||
| if ($field->nullable) { | |||||
| $template .= " DEFAULT NULL"; | |||||
| } | |||||
| if ($field->default !== null) { | |||||
| $template .= " DEFAULT '{$field->default}'"; | |||||
| } | |||||
| return $template; | |||||
| } | |||||
| if ($field instanceof TextField) { | |||||
| return "{$prefix} `{$field->name}` text"; | |||||
| } | |||||
| throw new MigrationException("Unrecognised field type for {$field->name}"); | |||||
| } | |||||
| public function dropColumn(string $name): static | |||||
| { | |||||
| $this->drops[] = $name; | |||||
| return $this; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,154 @@ | |||||
| <?php | |||||
| namespace Framework\Database\Migration; | |||||
| use Framework\Database\Connection\SqliteConnection; | |||||
| use Framework\Database\Exception\MigrationException; | |||||
| use Framework\Database\Migration\Field\Field; | |||||
| use Framework\Database\Migration\Field\BoolField; | |||||
| use Framework\Database\Migration\Field\DateTimeField; | |||||
| use Framework\Database\Migration\Field\FloatField; | |||||
| use Framework\Database\Migration\Field\IdField; | |||||
| use Framework\Database\Migration\Field\IntField; | |||||
| use Framework\Database\Migration\Field\StringField; | |||||
| use Framework\Database\Migration\Field\TextField; | |||||
| class SqliteMigration extends Migration | |||||
| { | |||||
| protected SqliteConnection $connection; | |||||
| protected string $table; | |||||
| protected string $type; | |||||
| public function __construct(SqliteConnection $connection, string $table, string $type) | |||||
| { | |||||
| $this->connection = $connection; | |||||
| $this->table = $table; | |||||
| $this->type = $type; | |||||
| } | |||||
| public function execute() | |||||
| { | |||||
| $command = $this->type === 'create' ? '' : 'ALTER TABLE'; | |||||
| $fields = array_map(fn($field) => $this->stringForField($field), $this->fields); | |||||
| if ($this->type === 'create') { | |||||
| $fields = join(',' . PHP_EOL, $fields); | |||||
| $query = " | |||||
| CREATE TABLE \"{$this->table}\" ( | |||||
| {$fields} | |||||
| ); | |||||
| "; | |||||
| } | |||||
| if ($this->type === 'alter') { | |||||
| $fields = join(';' . PHP_EOL, $fields); | |||||
| $query = " | |||||
| ALTER TABLE \"{$this->table}\" | |||||
| {$fields}; | |||||
| "; | |||||
| } | |||||
| $statement = $this->connection->pdo()->prepare($query); | |||||
| $statement->execute(); | |||||
| } | |||||
| private function stringForField(Field $field): string | |||||
| { | |||||
| $prefix = ''; | |||||
| if ($this->type === 'alter') { | |||||
| $prefix = 'ADD COLUMN'; | |||||
| } | |||||
| if ($field->alter) { | |||||
| throw new MigrationException('SQLite doesn\'t support altering columns'); | |||||
| } | |||||
| if ($field instanceof BoolField) { | |||||
| $template = "{$prefix} \"{$field->name}\" INTEGER"; | |||||
| if (!$field->nullable) { | |||||
| $template .= " NOT NULL"; | |||||
| } | |||||
| if ($field->default !== null) { | |||||
| $default = (int) $field->default; | |||||
| $template .= " DEFAULT {$default}"; | |||||
| } | |||||
| return $template; | |||||
| } | |||||
| if ($field instanceof DateTimeField) { | |||||
| $template = "{$prefix} \"{$field->name}\" TEXT"; | |||||
| if (!$field->nullable) { | |||||
| $template .= " NOT NULL"; | |||||
| } | |||||
| if ($field->default === 'CURRENT_TIMESTAMP') { | |||||
| $template .= " DEFAULT CURRENT_TIMESTAMP"; | |||||
| } else if ($field->default !== null) { | |||||
| $template .= " DEFAULT '{$field->default}'"; | |||||
| } | |||||
| return $template; | |||||
| } | |||||
| if ($field instanceof FloatField) { | |||||
| $template = "{$prefix} \"{$field->name}\" REAL"; | |||||
| if (!$field->nullable) { | |||||
| $template .= " NOT NULL"; | |||||
| } | |||||
| if ($field->default !== null) { | |||||
| $template .= " DEFAULT {$field->default}"; | |||||
| } | |||||
| return $template; | |||||
| } | |||||
| if ($field instanceof IdField) { | |||||
| return "{$prefix} \"{$field->name}\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE"; | |||||
| } | |||||
| if ($field instanceof IntField) { | |||||
| $template = "{$prefix} \"{$field->name}\" INTEGER"; | |||||
| if (!$field->nullable) { | |||||
| $template .= " NOT NULL"; | |||||
| } | |||||
| if ($field->default !== null) { | |||||
| $template .= " DEFAULT {$field->default}"; | |||||
| } | |||||
| return $template; | |||||
| } | |||||
| if ($field instanceof StringField || $field instanceof TextField) { | |||||
| $template = "{$prefix} \"{$field->name}\" TEXT"; | |||||
| if (!$field->nullable) { | |||||
| $template .= " NOT NULL"; | |||||
| } | |||||
| if ($field->default !== null) { | |||||
| $template .= " DEFAULT '{$field->default}'"; | |||||
| } | |||||
| return $template; | |||||
| } | |||||
| throw new MigrationException("Unrecognised field type for {$field->name}"); | |||||
| } | |||||
| public function dropColumn(string $name): static | |||||
| { | |||||
| throw new MigrationException('SQLite doesn\'t support dropping columns'); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,189 @@ | |||||
| <?php | |||||
| namespace Framework\Database; | |||||
| use Exception; | |||||
| use Framework\Database\Connection\Connection; | |||||
| use Framework\Database\Connection\MysqlConnection; | |||||
| use Framework\Database\Connection\SqliteConnection; | |||||
| use Framework\Database\Exception\ConnectionException; | |||||
| use ReflectionClass; | |||||
| abstract class Model | |||||
| { | |||||
| protected Connection $connection; | |||||
| protected string $table; | |||||
| protected array $attributes = []; | |||||
| protected array $dirty = []; | |||||
| protected array $casts = []; | |||||
| public function setConnection(Connection $connection): static | |||||
| { | |||||
| $this->connection = $connection; | |||||
| return $this; | |||||
| } | |||||
| public function getConnection(): Connection | |||||
| { | |||||
| if (!isset($this->connection)) { | |||||
| $this->connection = app('database'); | |||||
| } | |||||
| return $this->connection; | |||||
| } | |||||
| public function setTable(string $table): static | |||||
| { | |||||
| $this->table = $table; | |||||
| return $this; | |||||
| } | |||||
| public function getTable(): string | |||||
| { | |||||
| if (!isset($this->table)) { | |||||
| $reflector = new ReflectionClass(static::class); | |||||
| foreach ($reflector->getAttributes() as $attribute) { | |||||
| if ($attribute->getName() == TableName::class) { | |||||
| return $attribute->getArguments()[0]; | |||||
| } | |||||
| } | |||||
| throw new Exception('$table is not set and getTable is not defined'); | |||||
| } | |||||
| return $this->table; | |||||
| } | |||||
| public static function with(array $attributes = []): static | |||||
| { | |||||
| $model = new static(); | |||||
| $model->attributes = $attributes; | |||||
| return $model; | |||||
| } | |||||
| public static function query(): mixed | |||||
| { | |||||
| $model = new static(); | |||||
| $query = $model->getConnection()->query(); | |||||
| return (new ModelCollector($query, static::class)) | |||||
| ->from($model->getTable()); | |||||
| } | |||||
| public static function __callStatic(string $method, array $parameters = []): mixed | |||||
| { | |||||
| return static::query()->$method(...$parameters); | |||||
| } | |||||
| public function __get(string $property): mixed | |||||
| { | |||||
| $getter = 'get' . ucfirst($property) . 'Attribute'; | |||||
| $value = null; | |||||
| if (method_exists($this, $property)) { | |||||
| $relationship = $this->$property(); | |||||
| $method = $relationship->method; | |||||
| $value = $relationship->$method(); | |||||
| } | |||||
| if (method_exists($this, $getter)) { | |||||
| $value = $this->$getter($this->attributes[$property] ?? null); | |||||
| } | |||||
| if (isset($this->attributes[$property])) { | |||||
| $value = $this->attributes[$property]; | |||||
| } | |||||
| if (isset($this->casts[$property]) && is_callable($this->casts[$property])) { | |||||
| $value = $this->casts[$property]($value); | |||||
| } | |||||
| return $value; | |||||
| } | |||||
| public function __set(string $property, $value) | |||||
| { | |||||
| $setter = 'set' . ucfirst($property) . 'Attribute'; | |||||
| array_push($this->dirty, $property); | |||||
| if (method_exists($this, $setter)) { | |||||
| $this->attributes[$property] = $this->$setter($value); | |||||
| return; | |||||
| } | |||||
| $this->attributes[$property] = $value; | |||||
| } | |||||
| public function save(): static | |||||
| { | |||||
| $values = []; | |||||
| foreach ($this->dirty as $dirty) { | |||||
| $values[$dirty] = $this->attributes[$dirty]; | |||||
| } | |||||
| $data = [array_keys($values), $values]; | |||||
| $query = static::query(); | |||||
| if (isset($this->attributes['id'])) { | |||||
| $query | |||||
| ->where('id', $this->attributes['id']) | |||||
| ->update(...$data); | |||||
| return $this; | |||||
| } | |||||
| $query->insert(...$data); | |||||
| $this->attributes['id'] = $query->getLastInsertId(); | |||||
| $this->dirty = []; | |||||
| return $this; | |||||
| } | |||||
| public function delete(): static | |||||
| { | |||||
| if (isset($this->attributes['id'])) { | |||||
| static::query() | |||||
| ->where('id', $this->attributes['id']) | |||||
| ->delete(); | |||||
| } | |||||
| return $this; | |||||
| } | |||||
| public function hasOne(string $class, string $foreignKey, string $primaryKey = 'id'): mixed | |||||
| { | |||||
| $model = new $class; | |||||
| $query = $class::query()->from($model->getTable())->where($foreignKey, $this->attributes['id']); | |||||
| return new Relationship($query, 'first'); | |||||
| } | |||||
| public function hasMany(string $class, string $foreignKey, string $primaryKey = 'id'): mixed | |||||
| { | |||||
| $model = new $class; | |||||
| $query = $class::query()->from($model->getTable())->where($foreignKey, $this->attributes['id']); | |||||
| return new Relationship($query, 'all'); | |||||
| } | |||||
| public function belongsTo(string $class, string $foreignKey, string $primaryKey = 'id'): mixed | |||||
| { | |||||
| $model = new $class; | |||||
| $query = $class::query()->from($model->getTable())->where($primaryKey, $this->attributes[$foreignKey]); | |||||
| return new Relationship($query, 'first'); | |||||
| } | |||||
| public static function find(int $id): static | |||||
| { | |||||
| return static::where('id', $id)->first(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,56 @@ | |||||
| <?php | |||||
| namespace Framework\Database; | |||||
| use Framework\Database\QueryBuilder\QueryBuilder; | |||||
| class ModelCollector | |||||
| { | |||||
| private QueryBuilder $builder; | |||||
| private string $class; | |||||
| public function __construct(QueryBuilder $builder, string $class) | |||||
| { | |||||
| $this->builder = $builder; | |||||
| $this->class = $class; | |||||
| } | |||||
| public function __call(string $method, array $parameters = []): mixed | |||||
| { | |||||
| $result = $this->builder->$method(...$parameters); | |||||
| // in case it's a fluent method... | |||||
| if ($result instanceof QueryBuilder) { | |||||
| $this->builder = $result; | |||||
| return $this; | |||||
| } | |||||
| return $result; | |||||
| } | |||||
| public function first() | |||||
| { | |||||
| $class = $this->class; | |||||
| $row = $this->builder->first(); | |||||
| if (!is_null($row)) { | |||||
| $row = $class::with($row); | |||||
| } | |||||
| return $row; | |||||
| } | |||||
| public function all() | |||||
| { | |||||
| $class = $this->class; | |||||
| $rows = $this->builder->all(); | |||||
| foreach ($rows as $i => $row) { | |||||
| $rows[$i] = $class::with($row); | |||||
| } | |||||
| return $rows; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| <?php | |||||
| namespace Framework\Database\QueryBuilder; | |||||
| use Framework\Database\Connection\MysqlConnection; | |||||
| class MysqlQueryBuilder extends QueryBuilder | |||||
| { | |||||
| protected MysqlConnection $connection; | |||||
| public function __construct(MysqlConnection $connection) | |||||
| { | |||||
| $this->connection = $connection; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,307 @@ | |||||
| <?php | |||||
| namespace Framework\Database\QueryBuilder; | |||||
| use Framework\Database\Connection\Connection; | |||||
| use Framework\Database\Exception\QueryException; | |||||
| use Pdo; | |||||
| use PdoStatement; | |||||
| abstract class QueryBuilder | |||||
| { | |||||
| protected string $type; | |||||
| protected array $columns; | |||||
| protected string $table; | |||||
| protected int $limit; | |||||
| protected int $offset; | |||||
| protected array $values; | |||||
| protected array $wheres = []; | |||||
| /** | |||||
| * Fetch all rows matching the current query | |||||
| */ | |||||
| public function all(): array | |||||
| { | |||||
| if (!isset($this->type)) { | |||||
| $this->select(); | |||||
| } | |||||
| $statement = $this->prepare(); | |||||
| $statement->execute($this->getWhereValues()); | |||||
| return $statement->fetchAll(Pdo::FETCH_ASSOC); | |||||
| } | |||||
| /** | |||||
| * Get the values for the where clause placeholders | |||||
| */ | |||||
| protected function getWhereValues(): array | |||||
| { | |||||
| $values = []; | |||||
| if (count($this->wheres) === 0) { | |||||
| return $values; | |||||
| } | |||||
| foreach ($this->wheres as $where) { | |||||
| if (is_bool($where[2])) { | |||||
| $values[$where[0]] = (int) $where[2]; | |||||
| continue; | |||||
| } | |||||
| $values[$where[0]] = $where[2]; | |||||
| } | |||||
| return $values; | |||||
| } | |||||
| /** | |||||
| * Prepare a query against a particular connection | |||||
| */ | |||||
| public function prepare(): PdoStatement | |||||
| { | |||||
| $query = ''; | |||||
| if ($this->type === 'select') { | |||||
| $query = $this->compileSelect($query); | |||||
| $query = $this->compileWheres($query); | |||||
| $query = $this->compileLimit($query); | |||||
| } | |||||
| if ($this->type === 'insert') { | |||||
| $query = $this->compileInsert($query); | |||||
| } | |||||
| if ($this->type === 'update') { | |||||
| $query = $this->compileUpdate($query); | |||||
| $query = $this->compileWheres($query); | |||||
| } | |||||
| if ($this->type === 'delete') { | |||||
| $query = $this->compileDelete($query); | |||||
| $query = $this->compileWheres($query); | |||||
| } | |||||
| if (empty($query)) { | |||||
| throw new QueryException('Unrecognised query type'); | |||||
| } | |||||
| return $this->connection->pdo()->prepare($query); | |||||
| } | |||||
| /** | |||||
| * Add select clause to the query | |||||
| */ | |||||
| protected function compileSelect(string $query): string | |||||
| { | |||||
| $joinedColumns = join(', ', $this->columns); | |||||
| $query .= " SELECT {$joinedColumns} FROM {$this->table}"; | |||||
| return $query; | |||||
| } | |||||
| /** | |||||
| * Add limit and offset clauses to the query | |||||
| */ | |||||
| protected function compileLimit(string $query): string | |||||
| { | |||||
| if (isset($this->limit)) { | |||||
| $query .= " LIMIT {$this->limit}"; | |||||
| } | |||||
| if (isset($this->offset)) { | |||||
| $query .= " OFFSET {$this->offset}"; | |||||
| } | |||||
| return $query; | |||||
| } | |||||
| /** | |||||
| * Add where clauses to the query | |||||
| */ | |||||
| protected function compileWheres(string $query): string | |||||
| { | |||||
| if (count($this->wheres) === 0) { | |||||
| return $query; | |||||
| } | |||||
| $query .= ' WHERE'; | |||||
| foreach ($this->wheres as $i => $where) { | |||||
| if ($i > 0) { | |||||
| $query .= ' AND '; | |||||
| } | |||||
| [$column, $comparator, $value] = $where; | |||||
| $query .= " {$column} {$comparator} :{$column}"; | |||||
| } | |||||
| return $query; | |||||
| } | |||||
| /** | |||||
| * Add insert clause to the query | |||||
| */ | |||||
| protected function compileInsert(string $query): string | |||||
| { | |||||
| $joinedColumns = join(', ', $this->columns); | |||||
| $joinedPlaceholders = join(', ', array_map(fn($column) => ":{$column}", $this->columns)); | |||||
| $query .= " INSERT INTO {$this->table} ({$joinedColumns}) VALUES ({$joinedPlaceholders})"; | |||||
| return $query; | |||||
| } | |||||
| /** | |||||
| * Add update clause to the query | |||||
| */ | |||||
| protected function compileUpdate(string $query): string | |||||
| { | |||||
| $joinedColumns = ''; | |||||
| foreach ($this->columns as $i => $column) { | |||||
| if ($i > 0) { | |||||
| $joinedColumns .= ', '; | |||||
| } | |||||
| $joinedColumns = " {$column} = :{$column}"; | |||||
| } | |||||
| $query .= " UPDATE {$this->table} SET {$joinedColumns}"; | |||||
| return $query; | |||||
| } | |||||
| /** | |||||
| * Add delete clause to the query | |||||
| */ | |||||
| protected function compileDelete(string $query): string | |||||
| { | |||||
| $query .= " DELETE FROM {$this->table}"; | |||||
| return $query; | |||||
| } | |||||
| /** | |||||
| * Fetch the first row matching the current query | |||||
| */ | |||||
| public function first(): ?array | |||||
| { | |||||
| if (!isset($this->type)) { | |||||
| $this->select(); | |||||
| } | |||||
| $statement = $this->take(1)->prepare(); | |||||
| $statement->execute($this->getWhereValues()); | |||||
| $result = $statement->fetchAll(Pdo::FETCH_ASSOC); | |||||
| if (count($result) === 1) { | |||||
| return $result[0]; | |||||
| } | |||||
| return null; | |||||
| } | |||||
| /** | |||||
| * Limit a set of query results so that it's possible | |||||
| * to fetch a single or limited batch of rows | |||||
| */ | |||||
| public function take(int $limit, int $offset = 0): static | |||||
| { | |||||
| $this->limit = $limit; | |||||
| $this->offset = $offset; | |||||
| return $this; | |||||
| } | |||||
| /** | |||||
| * Indicate which table the query is targetting | |||||
| */ | |||||
| public function from(string $table): static | |||||
| { | |||||
| $this->table = $table; | |||||
| return $this; | |||||
| } | |||||
| /** | |||||
| * Indicate the query type is a "select" and remember | |||||
| * which fields should be returned by the query | |||||
| */ | |||||
| public function select(mixed $columns = '*'): static | |||||
| { | |||||
| if (is_string($columns)) { | |||||
| $columns = [$columns]; | |||||
| } | |||||
| $this->type = 'select'; | |||||
| $this->columns = $columns; | |||||
| return $this; | |||||
| } | |||||
| /** | |||||
| * Insert a row of data into the table specified in the query | |||||
| * and return the number of affected rows | |||||
| */ | |||||
| public function insert(array $columns, array $values): int | |||||
| { | |||||
| $this->type = 'insert'; | |||||
| $this->columns = $columns; | |||||
| $this->values = $values; | |||||
| $statement = $this->prepare(); | |||||
| return $statement->execute($values); | |||||
| } | |||||
| /** | |||||
| * Store where clause data for later queries | |||||
| */ | |||||
| public function where(string $column, mixed $comparator, mixed $value = null): static | |||||
| { | |||||
| if (is_null($value) && !is_null($comparator)) { | |||||
| array_push($this->wheres, [$column, '=', $comparator]); | |||||
| } else { | |||||
| array_push($this->wheres, [$column, $comparator, $value]); | |||||
| } | |||||
| return $this; | |||||
| } | |||||
| /** | |||||
| * Insert a row of data into the table specified in the query | |||||
| * and return the number of affected rows | |||||
| */ | |||||
| public function update(array $columns, array $values): int | |||||
| { | |||||
| $this->type = 'update'; | |||||
| $this->columns = $columns; | |||||
| $this->values = $values; | |||||
| $statement = $this->prepare(); | |||||
| return $statement->execute($this->getWhereValues() + $values); | |||||
| } | |||||
| /** | |||||
| * Get the ID of the last row that was inserted | |||||
| */ | |||||
| public function getLastInsertId(): string | |||||
| { | |||||
| return $this->connection->pdo()->lastInsertId(); | |||||
| } | |||||
| /** | |||||
| * Delete a row from the database | |||||
| */ | |||||
| public function delete(): int | |||||
| { | |||||
| $this->type = 'delete'; | |||||
| $statement = $this->prepare(); | |||||
| return $statement->execute($this->getWhereValues()); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| <?php | |||||
| namespace Framework\Database\QueryBuilder; | |||||
| use Framework\Database\Connection\SqliteConnection; | |||||
| class SqliteQueryBuilder extends QueryBuilder | |||||
| { | |||||
| protected SqliteConnection $connection; | |||||
| public function __construct(SqliteConnection $connection) | |||||
| { | |||||
| $this->connection = $connection; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,27 @@ | |||||
| <?php | |||||
| namespace Framework\Database; | |||||
| use Framework\Database\ModelCollector; | |||||
| class Relationship | |||||
| { | |||||
| public ModelCollector $collector; | |||||
| public string $method; | |||||
| public function __construct(ModelCollector $collector, string $method) | |||||
| { | |||||
| $this->collector = $collector; | |||||
| $this->method = $method; | |||||
| } | |||||
| public function __invoke(array $parameters = []): mixed | |||||
| { | |||||
| return $this->collector->$method(...$parameters); | |||||
| } | |||||
| public function __call(string $method, array $parameters = []): mixed | |||||
| { | |||||
| return $this->collector->$method(...$parameters); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| <?php | |||||
| namespace Framework\Database; | |||||
| #[Attribute] | |||||
| class TableName | |||||
| { | |||||
| public string $name; | |||||
| public function __construct(string $name) | |||||
| { | |||||
| $this->name = $name; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,12 @@ | |||||
| <?php | |||||
| namespace Framework\Email\Driver; | |||||
| interface Driver | |||||
| { | |||||
| public function to(string $to): static; | |||||
| public function subject(string $subject): static; | |||||
| public function text(string $text): static; | |||||
| public function html(string $html): static; | |||||
| public function send(): void; | |||||
| } | |||||
| @@ -0,0 +1,93 @@ | |||||
| <?php | |||||
| namespace Framework\Email\Driver; | |||||
| use Framework\Email\Exception\CompositionException; | |||||
| use Postmark\Transport; | |||||
| use Swift_Mailer; | |||||
| use Swift_Message; | |||||
| class PostmarkDriver implements Driver | |||||
| { | |||||
| private array $config; | |||||
| private Swift_Mailer $mailer; | |||||
| private string $to; | |||||
| private string $subject; | |||||
| private string $text; | |||||
| private string $html; | |||||
| public function __construct(array $config) | |||||
| { | |||||
| $this->config = $config; | |||||
| } | |||||
| public function to(string $to): static | |||||
| { | |||||
| $this->to = $to; | |||||
| return $this; | |||||
| } | |||||
| public function subject(string $subject): static | |||||
| { | |||||
| $this->subject = $subject; | |||||
| return $this; | |||||
| } | |||||
| public function text(string $text): static | |||||
| { | |||||
| $this->text = $text; | |||||
| return $this; | |||||
| } | |||||
| public function html(string $html): static | |||||
| { | |||||
| $this->html = $html; | |||||
| return $this; | |||||
| } | |||||
| public function send(): void | |||||
| { | |||||
| if (!isset($this->to)) { | |||||
| throw new CompositionException('to required'); | |||||
| } | |||||
| if (!isset($this->text) && !isset($this->html)) { | |||||
| throw new CompositionException('text or email required'); | |||||
| } | |||||
| $fromName = $this->config['from']['name']; | |||||
| $fromEmail = $this->config['from']['email']; | |||||
| $subject = $this->subject ?? "Message from {$fromName}"; | |||||
| $message = (new Swift_Message($subject)) | |||||
| ->setFrom([$fromEmail => $fromName]) | |||||
| ->setTo([$this->to]); | |||||
| if (isset($this->text) && !isset($this->html)) { | |||||
| $message->setBody($this->text, 'text/plain'); | |||||
| } | |||||
| if (!isset($this->text) && isset($this->html)) { | |||||
| $message->setBody($this->html, 'text/html'); | |||||
| } | |||||
| if (isset($this->text, $this->html)) { | |||||
| $message | |||||
| ->setBody($this->html, 'text/html') | |||||
| ->addPart($this->text, 'text/plain'); | |||||
| } | |||||
| $this->mailer()->send($message); | |||||
| } | |||||
| private function mailer() | |||||
| { | |||||
| if (!isset($this->mailer)) { | |||||
| $transport = new Transport($this->config['token']); | |||||
| $this->mailer = new Swift_Mailer($transport); | |||||
| } | |||||
| return $this->mailer; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| <?php | |||||
| namespace Framework\Email\Exception; | |||||
| use InvalidArgumentException; | |||||
| class CompositionException extends InvalidArgumentException | |||||
| { | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| <?php | |||||
| namespace Framework\Email\Exception; | |||||
| use RuntimeException; | |||||
| class DriverException extends RuntimeException | |||||
| { | |||||
| } | |||||
| @@ -0,0 +1,34 @@ | |||||
| <?php | |||||
| namespace Framework\Email; | |||||
| use Closure; | |||||
| use Framework\Email\Driver\Driver; | |||||
| use Framework\Email\Exception\DriverException; | |||||
| use Framework\Support\DriverFactory; | |||||
| class Factory implements DriverFactory | |||||
| { | |||||
| protected array $drivers; | |||||
| public function addDriver(string $alias, Closure $driver): static | |||||
| { | |||||
| $this->drivers[$alias] = $driver; | |||||
| return $this; | |||||
| } | |||||
| public function connect(array $config): Driver | |||||
| { | |||||
| if (!isset($config['type'])) { | |||||
| throw new DriverException('type is not defined'); | |||||
| } | |||||
| $type = $config['type']; | |||||
| if (isset($this->drivers[$type])) { | |||||
| return $this->drivers[$type]($config); | |||||
| } | |||||
| throw new DriverException('unrecognised type'); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,44 @@ | |||||
| <?php | |||||
| namespace Framework\Filesystem\Driver; | |||||
| use League\Flysystem\Filesystem; | |||||
| abstract class Driver | |||||
| { | |||||
| protected Filesystem $filesystem; | |||||
| public function __construct(array $config) | |||||
| { | |||||
| $this->filesystem = $this->connect($config); | |||||
| } | |||||
| abstract protected function connect(array $config): Filesystem; | |||||
| public function list(string $path, bool $recursive = false): iterable | |||||
| { | |||||
| return $this->filesystem->listContents($path, $recursive); | |||||
| } | |||||
| public function exists(string $path): bool | |||||
| { | |||||
| return $this->filesystem->fileExists($path); | |||||
| } | |||||
| public function get(string $path): string | |||||
| { | |||||
| return $this->filesystem->read($path); | |||||
| } | |||||
| public function put(string $path, mixed $value): static | |||||
| { | |||||
| $this->filesystem->write($path, $value); | |||||
| return $this; | |||||
| } | |||||
| public function delete(string $path): static | |||||
| { | |||||
| $this->filesystem->delete($path); | |||||
| return $this; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| <?php | |||||
| namespace Framework\Filesystem\Driver; | |||||
| use League\Flysystem\Filesystem; | |||||
| use League\Flysystem\Local\LocalFilesystemAdapter; | |||||
| class LocalDriver extends Driver | |||||
| { | |||||
| protected function connect(array $config): Filesystem | |||||
| { | |||||
| $adapter = new LocalFilesystemAdapter($config['path']); | |||||
| return new Filesystem($adapter); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| <?php | |||||
| namespace Framework\Filesystem\Exception; | |||||
| use RuntimeException; | |||||
| class DriverException extends RuntimeException | |||||
| { | |||||
| } | |||||
| @@ -0,0 +1,34 @@ | |||||
| <?php | |||||
| namespace Framework\Filesystem; | |||||
| use Closure; | |||||
| use Framework\Filesystem\Driver\Driver; | |||||
| use Framework\Filesystem\Exception\DriverException; | |||||
| use Framework\Support\DriverFactory; | |||||
| class Factory implements DriverFactory | |||||
| { | |||||
| protected array $drivers; | |||||
| public function addDriver(string $alias, Closure $driver): static | |||||
| { | |||||
| $this->drivers[$alias] = $driver; | |||||
| return $this; | |||||
| } | |||||
| public function connect(array $config): Driver | |||||
| { | |||||
| if (!isset($config['type'])) { | |||||
| throw new DriverException('type is not defined'); | |||||
| } | |||||
| $type = $config['type']; | |||||
| if (isset($this->drivers[$type])) { | |||||
| return $this->drivers[$type]($config); | |||||
| } | |||||
| throw new DriverException('unrecognised type'); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,104 @@ | |||||
| <?php | |||||
| namespace Framework\Http; | |||||
| use InvalidArgumentException; | |||||
| class Response | |||||
| { | |||||
| const REDIRECT = 'REDIRECT'; | |||||
| const HTML = 'HTML'; | |||||
| const JSON = 'JSON'; | |||||
| private string $type = 'HTML'; | |||||
| private ?string $redirect = null; | |||||
| private mixed $content = ''; | |||||
| private int $status = 200; | |||||
| private array $headers = []; | |||||
| public function content(mixed $content = null): mixed | |||||
| { | |||||
| if (is_null($content)) { | |||||
| return $this->content; | |||||
| } | |||||
| $this->content = $content; | |||||
| return $this; | |||||
| } | |||||
| public function status(int $status = null): int|static | |||||
| { | |||||
| if (is_null($status)) { | |||||
| return $this->status; | |||||
| } | |||||
| $this->status = $status; | |||||
| return $this; | |||||
| } | |||||
| public function header(string $key, string $value): static | |||||
| { | |||||
| $this->headers[$key] = $value; | |||||
| return $this; | |||||
| } | |||||
| public function redirect(string $redirect = null): mixed | |||||
| { | |||||
| if (is_null($redirect)) { | |||||
| return $this->redirect; | |||||
| } | |||||
| $this->redirect = $redirect; | |||||
| $this->type = static::REDIRECT; | |||||
| return $this; | |||||
| } | |||||
| public function json(mixed $content): static | |||||
| { | |||||
| $this->content = $content; | |||||
| $this->type = static::JSON; | |||||
| return $this; | |||||
| } | |||||
| public function type(string $type = null): string|static | |||||
| { | |||||
| if (is_null($type)) { | |||||
| return $this->type; | |||||
| } | |||||
| $this->type = $type; | |||||
| return $this; | |||||
| } | |||||
| public function send(): void | |||||
| { | |||||
| foreach ($this->headers as $key => $value) { | |||||
| header("{$key}: {$value}"); | |||||
| } | |||||
| if ($this->type === static::HTML) { | |||||
| header('Content-Type: text/html'); | |||||
| http_response_code($this->status); | |||||
| print $this->content; | |||||
| return; | |||||
| } | |||||
| if ($this->type === static::JSON) { | |||||
| header('Content-Type: application/json'); | |||||
| http_response_code($this->status); | |||||
| print json_encode($this->content); | |||||
| return; | |||||
| } | |||||
| if ($this->type === static::REDIRECT) { | |||||
| header("Location: {$this->redirect}"); | |||||
| return; | |||||
| } | |||||
| throw new InvalidArgumentException("{$this->type} is not a recognised type"); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,10 @@ | |||||
| <?php | |||||
| namespace Framework\Logging\Driver; | |||||
| interface Driver | |||||
| { | |||||
| public function info(string $message): static; | |||||
| public function warning(string $message): static; | |||||
| public function error(string $message): static; | |||||
| } | |||||
| @@ -0,0 +1,44 @@ | |||||
| <?php | |||||
| namespace Framework\Logging\Driver; | |||||
| use Monolog\Logger; | |||||
| use Monolog\Handler\StreamHandler; | |||||
| class StreamDriver implements Driver | |||||
| { | |||||
| private array $config; | |||||
| public function __construct(array $config) | |||||
| { | |||||
| $this->config = $config; | |||||
| } | |||||
| public function info(string $message): static | |||||
| { | |||||
| $this->logger()->info($message); | |||||
| return $this; | |||||
| } | |||||
| private function logger() | |||||
| { | |||||
| if (!isset($this->logger)) { | |||||
| $this->logger = new Logger($this->config['name']); | |||||
| $this->logger->pushHandler(new StreamHandler($this->config['path'], $this->config['minimum'])); | |||||
| } | |||||
| return $this->logger; | |||||
| } | |||||
| public function warning(string $message): static | |||||
| { | |||||
| $this->logger()->warning($message); | |||||
| return $this; | |||||
| } | |||||
| public function error(string $message): static | |||||
| { | |||||
| $this->logger()->error($message); | |||||
| return $this; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| <?php | |||||
| namespace Framework\Logging\Exception; | |||||
| use RuntimeException; | |||||
| class DriverException extends RuntimeException | |||||
| { | |||||
| } | |||||
| @@ -0,0 +1,34 @@ | |||||
| <?php | |||||
| namespace Framework\Logging; | |||||
| use Closure; | |||||
| use Framework\Logging\Driver\Driver; | |||||
| use Framework\Logging\Exception\DriverException; | |||||
| use Framework\Support\DriverFactory; | |||||
| class Factory implements DriverFactory | |||||
| { | |||||
| protected array $drivers; | |||||
| public function addDriver(string $alias, Closure $driver): static | |||||
| { | |||||
| $this->drivers[$alias] = $driver; | |||||
| return $this; | |||||
| } | |||||
| public function connect(array $config): Driver | |||||
| { | |||||
| if (!isset($config['type'])) { | |||||
| throw new DriverException('type is not defined'); | |||||
| } | |||||
| $type = $config['type']; | |||||
| if (isset($this->drivers[$type])) { | |||||
| return $this->drivers[$type]($config); | |||||
| } | |||||
| throw new DriverException('unrecognised type'); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,38 @@ | |||||
| <?php | |||||
| namespace Framework\Provider; | |||||
| use Framework\Cache\Factory; | |||||
| use Framework\Cache\Driver\FileDriver; | |||||
| use Framework\Cache\Driver\MemcacheDriver; | |||||
| use Framework\Cache\Driver\MemoryDriver; | |||||
| use Framework\Support\DriverProvider; | |||||
| use Framework\Support\DriverFactory; | |||||
| class CacheProvider extends DriverProvider | |||||
| { | |||||
| protected function name(): string | |||||
| { | |||||
| return 'cache'; | |||||
| } | |||||
| protected function factory(): DriverFactory | |||||
| { | |||||
| return new Factory(); | |||||
| } | |||||
| protected function drivers(): array | |||||
| { | |||||
| return [ | |||||
| 'file' => function($config) { | |||||
| return new FileDriver($config); | |||||
| }, | |||||
| 'memcache' => function($config) { | |||||
| return new MemcacheDriver($config); | |||||
| }, | |||||
| 'memory' => function($config) { | |||||
| return new MemoryDriver($config); | |||||
| }, | |||||
| ]; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| <?php | |||||
| namespace Framework\Provider; | |||||
| use Framework\App; | |||||
| use Framework\Support\Config; | |||||
| class ConfigProvider | |||||
| { | |||||
| public function bind(App $app): void | |||||
| { | |||||
| $app->bind('config', function($app) { | |||||
| return new Config(); | |||||
| }); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,34 @@ | |||||
| <?php | |||||
| namespace Framework\Provider; | |||||
| use Framework\Database\Factory; | |||||
| use Framework\Database\Connection\MysqlConnection; | |||||
| use Framework\Database\Connection\SqliteConnection; | |||||
| use Framework\Support\DriverProvider; | |||||
| use Framework\Support\DriverFactory; | |||||
| class DatabaseProvider extends DriverProvider | |||||
| { | |||||
| protected function name(): string | |||||
| { | |||||
| return 'database'; | |||||
| } | |||||
| protected function factory(): DriverFactory | |||||
| { | |||||
| return new Factory(); | |||||
| } | |||||
| protected function drivers(): array | |||||
| { | |||||
| return [ | |||||
| 'sqlite' => function($config) { | |||||
| return new SqliteConnection($config); | |||||
| }, | |||||
| 'mysql' => function($config) { | |||||
| return new MysqlConnection($config); | |||||
| }, | |||||
| ]; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,30 @@ | |||||
| <?php | |||||
| namespace Framework\Provider; | |||||
| use Framework\Email\Factory; | |||||
| use Framework\Email\Driver\PostmarkDriver; | |||||
| use Framework\Support\DriverProvider; | |||||
| use Framework\Support\DriverFactory; | |||||
| class EmailProvider extends DriverProvider | |||||
| { | |||||
| protected function name(): string | |||||
| { | |||||
| return 'email'; | |||||
| } | |||||
| protected function factory(): DriverFactory | |||||
| { | |||||
| return new Factory(); | |||||
| } | |||||
| protected function drivers(): array | |||||
| { | |||||
| return [ | |||||
| 'postmark' => function($config) { | |||||
| return new PostmarkDriver($config); | |||||
| }, | |||||
| ]; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,30 @@ | |||||
| <?php | |||||
| namespace Framework\Provider; | |||||
| use Framework\Filesystem\Factory; | |||||
| use Framework\Filesystem\Driver\LocalDriver; | |||||
| use Framework\Support\DriverProvider; | |||||
| use Framework\Support\DriverFactory; | |||||
| class FilesystemProvider extends DriverProvider | |||||
| { | |||||
| protected function name(): string | |||||
| { | |||||
| return 'filesystem'; | |||||
| } | |||||
| protected function factory(): DriverFactory | |||||
| { | |||||
| return new Factory(); | |||||
| } | |||||
| protected function drivers(): array | |||||
| { | |||||
| return [ | |||||
| 'local' => function($config) { | |||||
| return new LocalDriver($config); | |||||
| }, | |||||
| ]; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,30 @@ | |||||
| <?php | |||||
| namespace Framework\Provider; | |||||
| use Framework\Logging\Factory; | |||||
| use Framework\Logging\Driver\StreamDriver; | |||||
| use Framework\Support\DriverProvider; | |||||
| use Framework\Support\DriverFactory; | |||||
| class LoggingProvider extends DriverProvider | |||||
| { | |||||
| protected function name(): string | |||||
| { | |||||
| return 'logging'; | |||||
| } | |||||
| protected function factory(): DriverFactory | |||||
| { | |||||
| return new Factory(); | |||||
| } | |||||
| protected function drivers(): array | |||||
| { | |||||
| return [ | |||||
| 'stream' => function($config) { | |||||
| return new StreamDriver($config); | |||||
| }, | |||||
| ]; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,30 @@ | |||||
| <?php | |||||
| namespace Framework\Provider; | |||||
| use Framework\Queue\Factory; | |||||
| use Framework\Queue\Driver\DatabaseDriver; | |||||
| use Framework\Support\DriverProvider; | |||||
| use Framework\Support\DriverFactory; | |||||
| class QueueProvider extends DriverProvider | |||||
| { | |||||
| protected function name(): string | |||||
| { | |||||
| return 'queue'; | |||||
| } | |||||
| protected function factory(): DriverFactory | |||||
| { | |||||
| return new Factory(); | |||||
| } | |||||
| protected function drivers(): array | |||||
| { | |||||
| return [ | |||||
| 'database' => function($config) { | |||||
| return new DatabaseDriver($config); | |||||
| }, | |||||
| ]; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| <?php | |||||
| namespace Framework\Provider; | |||||
| use Framework\App; | |||||
| use Framework\Http\Response; | |||||
| class ResponseProvider | |||||
| { | |||||
| public function bind(App $app): void | |||||
| { | |||||
| $app->bind('response', function($app) { | |||||
| return new Response(); | |||||
| }); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,30 @@ | |||||
| <?php | |||||
| namespace Framework\Provider; | |||||
| use Framework\Session\Factory; | |||||
| use Framework\Session\Driver\NativeDriver; | |||||
| use Framework\Support\DriverProvider; | |||||
| use Framework\Support\DriverFactory; | |||||
| class SessionProvider extends DriverProvider | |||||
| { | |||||
| protected function name(): string | |||||
| { | |||||
| return 'session'; | |||||
| } | |||||
| protected function factory(): DriverFactory | |||||
| { | |||||
| return new Factory(); | |||||
| } | |||||
| protected function drivers(): array | |||||
| { | |||||
| return [ | |||||
| 'native' => function($config) { | |||||
| return new NativeDriver($config); | |||||
| }, | |||||
| ]; | |||||
| } | |||||
| } | |||||
Powered by TurnKey Linux.