Daniel Covington 1 неделю назад
Сommit
cc7c328f7e
100 измененных файлов: 8393 добавлений и 0 удалений
  1. +13
    -0
      .env.example
  2. +5
    -0
      .gitignore
  3. +23
    -0
      Dockerfile
  4. +16
    -0
      app/Exceptions/Handler.php
  5. +19
    -0
      app/Http/Controllers/Products/OrderProductController.php
  6. +22
    -0
      app/Http/Controllers/Products/ShowProductController.php
  7. +46
    -0
      app/Http/Controllers/ShowHomePageController.php
  8. +27
    -0
      app/Http/Controllers/Users/LogInUserController.php
  9. +15
    -0
      app/Http/Controllers/Users/LogOutUserController.php
  10. +34
    -0
      app/Http/Controllers/Users/RegisterUserController.php
  11. +17
    -0
      app/Http/Controllers/Users/ShowRegisterFormController.php
  12. +15
    -0
      app/Models/Order.php
  13. +27
    -0
      app/Models/Product.php
  14. +15
    -0
      app/Models/Profile.php
  15. +20
    -0
      app/Models/User.php
  16. +11
    -0
      app/commands.php
  17. +51
    -0
      app/routes.php
  18. +17
    -0
      command.php
  19. +36
    -0
      composer.json
  20. +4886
    -0
      composer.lock
  21. +19
    -0
      config/cache.php
  22. +17
    -0
      config/database.php
  23. +13
    -0
      config/email.php
  24. +24
    -0
      config/filesystem.php
  25. +5
    -0
      config/handlers.php
  26. +11
    -0
      config/logging.php
  27. +17
    -0
      config/providers.php
  28. +10
    -0
      config/queue.php
  29. +9
    -0
      config/session.php
  30. +18
    -0
      database/migrations/001_CreateOrdersTable.php
  31. +13
    -0
      database/migrations/002_AddDeliveryInstructions.php
  32. +13
    -0
      database/migrations/003_ChangeQuantity.php
  33. +13
    -0
      database/migrations/004_DropPrice.php
  34. +15
    -0
      database/migrations/005_CreateProductsTable.php
  35. +31
    -0
      database/migrations/006_SeedProducts.php
  36. +16
    -0
      database/migrations/007_CreateUsersTable.php
  37. +14
    -0
      database/migrations/008_CreateProfilesTable.php
  38. +13
    -0
      database/migrations/009_AddUserId.php
  39. +17
    -0
      database/migrations/010_CreateJobsTable.php
  40. +41
    -0
      docker-compose.yml
  41. +23
    -0
      docker/nginx/default.conf
  42. +3
    -0
      docker/php/app.ini
  43. +53
    -0
      docker/start-container.sh
  44. +78
    -0
      framework/App.php
  45. +31
    -0
      framework/Cache/Driver/Driver.php
  46. +111
    -0
      framework/Cache/Driver/FileDriver.php
  47. +55
    -0
      framework/Cache/Driver/MemcacheDriver.php
  48. +54
    -0
      framework/Cache/Driver/MemoryDriver.php
  49. +9
    -0
      framework/Cache/Exception/DriverException.php
  50. +34
    -0
      framework/Cache/Factory.php
  51. +80
    -0
      framework/Container.php
  52. +100
    -0
      framework/Database/Command/MigrateCommand.php
  53. +45
    -0
      framework/Database/Connection/Connection.php
  54. +94
    -0
      framework/Database/Connection/MysqlConnection.php
  55. +69
    -0
      framework/Database/Connection/SqliteConnection.php
  56. +9
    -0
      framework/Database/Exception/ConnectionException.php
  57. +9
    -0
      framework/Database/Exception/MigrationException.php
  58. +9
    -0
      framework/Database/Exception/QueryException.php
  59. +34
    -0
      framework/Database/Factory.php
  60. +14
    -0
      framework/Database/Migration/Field/BoolField.php
  61. +14
    -0
      framework/Database/Migration/Field/DateTimeField.php
  62. +27
    -0
      framework/Database/Migration/Field/Field.php
  63. +14
    -0
      framework/Database/Migration/Field/FloatField.php
  64. +13
    -0
      framework/Database/Migration/Field/IdField.php
  65. +14
    -0
      framework/Database/Migration/Field/IntField.php
  66. +14
    -0
      framework/Database/Migration/Field/StringField.php
  67. +21
    -0
      framework/Database/Migration/Field/TextField.php
  68. +62
    -0
      framework/Database/Migration/Migration.php
  69. +164
    -0
      framework/Database/Migration/MysqlMigration.php
  70. +154
    -0
      framework/Database/Migration/SqliteMigration.php
  71. +189
    -0
      framework/Database/Model.php
  72. +56
    -0
      framework/Database/ModelCollector.php
  73. +15
    -0
      framework/Database/QueryBuilder/MysqlQueryBuilder.php
  74. +307
    -0
      framework/Database/QueryBuilder/QueryBuilder.php
  75. +15
    -0
      framework/Database/QueryBuilder/SqliteQueryBuilder.php
  76. +27
    -0
      framework/Database/Relationship.php
  77. +14
    -0
      framework/Database/TableName.php
  78. +12
    -0
      framework/Email/Driver/Driver.php
  79. +93
    -0
      framework/Email/Driver/PostmarkDriver.php
  80. +9
    -0
      framework/Email/Exception/CompositionException.php
  81. +9
    -0
      framework/Email/Exception/DriverException.php
  82. +34
    -0
      framework/Email/Factory.php
  83. +44
    -0
      framework/Filesystem/Driver/Driver.php
  84. +16
    -0
      framework/Filesystem/Driver/LocalDriver.php
  85. +9
    -0
      framework/Filesystem/Exception/DriverException.php
  86. +34
    -0
      framework/Filesystem/Factory.php
  87. +104
    -0
      framework/Http/Response.php
  88. +10
    -0
      framework/Logging/Driver/Driver.php
  89. +44
    -0
      framework/Logging/Driver/StreamDriver.php
  90. +9
    -0
      framework/Logging/Exception/DriverException.php
  91. +34
    -0
      framework/Logging/Factory.php
  92. +38
    -0
      framework/Provider/CacheProvider.php
  93. +16
    -0
      framework/Provider/ConfigProvider.php
  94. +34
    -0
      framework/Provider/DatabaseProvider.php
  95. +30
    -0
      framework/Provider/EmailProvider.php
  96. +30
    -0
      framework/Provider/FilesystemProvider.php
  97. +30
    -0
      framework/Provider/LoggingProvider.php
  98. +30
    -0
      framework/Provider/QueueProvider.php
  99. +16
    -0
      framework/Provider/ResponseProvider.php
  100. +30
    -0
      framework/Provider/SessionProvider.php

+ 13
- 0
.env.example Просмотреть файл

@@ -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=

+ 5
- 0
.gitignore Просмотреть файл

@@ -0,0 +1,5 @@
.env
database/*.sqlite
drivers/*
vendor
*.cache

+ 23
- 0
Dockerfile Просмотреть файл

@@ -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"]

+ 16
- 0
app/Exceptions/Handler.php Просмотреть файл

@@ -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);
}
}

+ 19
- 0
app/Http/Controllers/Products/OrderProductController.php Просмотреть файл

@@ -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'));
}
}

+ 22
- 0
app/Http/Controllers/Products/ShowProductController.php Просмотреть файл

@@ -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(),
]);
}
}

+ 46
- 0
app/Http/Controllers/ShowHomePageController.php Просмотреть файл

@@ -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,
]);
}
}

+ 27
- 0
app/Http/Controllers/Users/LogInUserController.php Просмотреть файл

@@ -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'));
}
}

+ 15
- 0
app/Http/Controllers/Users/LogOutUserController.php Просмотреть файл

@@ -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'));
}
}

+ 34
- 0
app/Http/Controllers/Users/RegisterUserController.php Просмотреть файл

@@ -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'));
}
}

+ 17
- 0
app/Http/Controllers/Users/ShowRegisterFormController.php Просмотреть файл

@@ -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(),
]);
}
}

+ 15
- 0
app/Models/Order.php Просмотреть файл

@@ -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');
}
}

+ 27
- 0
app/Models/Product.php Просмотреть файл

@@ -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;
}
}

+ 15
- 0
app/Models/Profile.php Просмотреть файл

@@ -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');
}
}

+ 20
- 0
app/Models/User.php Просмотреть файл

@@ -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');
}
}

+ 11
- 0
app/commands.php Просмотреть файл

@@ -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,
];

+ 51
- 0
app/routes.php Просмотреть файл

@@ -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');
};

+ 17
- 0
command.php Просмотреть файл

@@ -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();

+ 36
- 0
composer.json Просмотреть файл

@@ -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"
}
}

+ 4886
- 0
composer.lock
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 19
- 0
config/cache.php Просмотреть файл

@@ -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,
],
];

+ 17
- 0
config/database.php Просмотреть файл

@@ -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'),
],
];

+ 13
- 0
config/email.php Просмотреть файл

@@ -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'),
],
]
];

+ 24
- 0
config/filesystem.php Просмотреть файл

@@ -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' => '',
],
];

+ 5
- 0
config/handlers.php Просмотреть файл

@@ -0,0 +1,5 @@
<?php

return [
'exceptions' => \App\Exceptions\Handler::class,
];

+ 11
- 0
config/logging.php Просмотреть файл

@@ -0,0 +1,11 @@
<?php

return [
'default' => 'stream',
'stream' => [
'type' => 'stream',
'path' => __DIR__ . '/../storage/app.log',
'name' => 'App',
'minimum' => \Monolog\Logger::DEBUG,
],
];

+ 17
- 0
config/providers.php Просмотреть файл

@@ -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,
];

+ 10
- 0
config/queue.php Просмотреть файл

@@ -0,0 +1,10 @@
<?php

return [
'default' => 'database',
'database' => [
'type' => 'database',
'table' => 'jobs',
'attempts' => 3,
],
];

+ 9
- 0
config/session.php Просмотреть файл

@@ -0,0 +1,9 @@
<?php

return [
'default' => 'native',
'native' => [
'type' => 'native',
'prefix' => 'framework_',
],
];

+ 18
- 0
database/migrations/001_CreateOrdersTable.php Просмотреть файл

@@ -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();
}
}

+ 13
- 0
database/migrations/002_AddDeliveryInstructions.php Просмотреть файл

@@ -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();
}
}

+ 13
- 0
database/migrations/003_ChangeQuantity.php Просмотреть файл

@@ -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();
}
}

+ 13
- 0
database/migrations/004_DropPrice.php Просмотреть файл

@@ -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();
}
}

+ 15
- 0
database/migrations/005_CreateProductsTable.php Просмотреть файл

@@ -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();
}
}

+ 31
- 0
database/migrations/006_SeedProducts.php Просмотреть файл

@@ -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&apos;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);
}
}
}

+ 16
- 0
database/migrations/007_CreateUsersTable.php Просмотреть файл

@@ -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();
}
}

+ 14
- 0
database/migrations/008_CreateProfilesTable.php Просмотреть файл

@@ -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();
}
}

+ 13
- 0
database/migrations/009_AddUserId.php Просмотреть файл

@@ -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();
}
}

+ 17
- 0
database/migrations/010_CreateJobsTable.php Просмотреть файл

@@ -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();
}
}

+ 41
- 0
docker-compose.yml Просмотреть файл

@@ -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:

+ 23
- 0
docker/nginx/default.conf Просмотреть файл

@@ -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;
}
}

+ 3
- 0
docker/php/app.ini Просмотреть файл

@@ -0,0 +1,3 @@
error_reporting = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED
display_errors = On
log_errors = On

+ 53
- 0
docker/start-container.sh Просмотреть файл

@@ -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;"

+ 78
- 0
framework/App.php Просмотреть файл

@@ -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;
}
}

+ 31
- 0
framework/Cache/Driver/Driver.php Просмотреть файл

@@ -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;
}

+ 111
- 0
framework/Cache/Driver/FileDriver.php Просмотреть файл

@@ -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;
}
}

+ 55
- 0
framework/Cache/Driver/MemcacheDriver.php Просмотреть файл

@@ -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;
}
}

+ 54
- 0
framework/Cache/Driver/MemoryDriver.php Просмотреть файл

@@ -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;
}
}

+ 9
- 0
framework/Cache/Exception/DriverException.php Просмотреть файл

@@ -0,0 +1,9 @@
<?php

namespace Framework\Cache\Exception;

use RuntimeException;

class DriverException extends RuntimeException
{
}

+ 34
- 0
framework/Cache/Factory.php Просмотреть файл

@@ -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');
}
}

+ 80
- 0
framework/Container.php Просмотреть файл

@@ -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);
}
}

+ 100
- 0
framework/Database/Command/MigrateCommand.php Просмотреть файл

@@ -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();
}
}

+ 45
- 0
framework/Database/Connection/Connection.php Просмотреть файл

@@ -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;
}

+ 94
- 0
framework/Database/Connection/MysqlConnection.php Просмотреть файл

@@ -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();
}
}

+ 69
- 0
framework/Database/Connection/SqliteConnection.php Просмотреть файл

@@ -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;
}
}

+ 9
- 0
framework/Database/Exception/ConnectionException.php Просмотреть файл

@@ -0,0 +1,9 @@
<?php

namespace Framework\Database\Exception;

use PDOException;

class ConnectionException extends PDOException
{
}

+ 9
- 0
framework/Database/Exception/MigrationException.php Просмотреть файл

@@ -0,0 +1,9 @@
<?php

namespace Framework\Database\Exception;

use PDOException;

class MigrationException extends PDOException
{
}

+ 9
- 0
framework/Database/Exception/QueryException.php Просмотреть файл

@@ -0,0 +1,9 @@
<?php

namespace Framework\Database\Exception;

use PDOException;

class ConnectionException extends PDOException
{
}

+ 34
- 0
framework/Database/Factory.php Просмотреть файл

@@ -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');
}
}

+ 14
- 0
framework/Database/Migration/Field/BoolField.php Просмотреть файл

@@ -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;
}
}

+ 14
- 0
framework/Database/Migration/Field/DateTimeField.php Просмотреть файл

@@ -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;
}
}

+ 27
- 0
framework/Database/Migration/Field/Field.php Просмотреть файл

@@ -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;
}
}

+ 14
- 0
framework/Database/Migration/Field/FloatField.php Просмотреть файл

@@ -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;
}
}

+ 13
- 0
framework/Database/Migration/Field/IdField.php Просмотреть файл

@@ -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');
}
}

+ 14
- 0
framework/Database/Migration/Field/IntField.php Просмотреть файл

@@ -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;
}
}

+ 14
- 0
framework/Database/Migration/Field/StringField.php Просмотреть файл

@@ -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;
}
}

+ 21
- 0
framework/Database/Migration/Field/TextField.php Просмотреть файл

@@ -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;
}
}

+ 62
- 0
framework/Database/Migration/Migration.php Просмотреть файл

@@ -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;
}

+ 164
- 0
framework/Database/Migration/MysqlMigration.php Просмотреть файл

@@ -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;
}
}

+ 154
- 0
framework/Database/Migration/SqliteMigration.php Просмотреть файл

@@ -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');
}
}

+ 189
- 0
framework/Database/Model.php Просмотреть файл

@@ -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();
}
}

+ 56
- 0
framework/Database/ModelCollector.php Просмотреть файл

@@ -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;
}
}

+ 15
- 0
framework/Database/QueryBuilder/MysqlQueryBuilder.php Просмотреть файл

@@ -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;
}
}

+ 307
- 0
framework/Database/QueryBuilder/QueryBuilder.php Просмотреть файл

@@ -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());
}
}

+ 15
- 0
framework/Database/QueryBuilder/SqliteQueryBuilder.php Просмотреть файл

@@ -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;
}
}

+ 27
- 0
framework/Database/Relationship.php Просмотреть файл

@@ -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);
}
}

+ 14
- 0
framework/Database/TableName.php Просмотреть файл

@@ -0,0 +1,14 @@
<?php

namespace Framework\Database;

#[Attribute]
class TableName
{
public string $name;

public function __construct(string $name)
{
$this->name = $name;
}
}

+ 12
- 0
framework/Email/Driver/Driver.php Просмотреть файл

@@ -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;
}

+ 93
- 0
framework/Email/Driver/PostmarkDriver.php Просмотреть файл

@@ -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;
}
}

+ 9
- 0
framework/Email/Exception/CompositionException.php Просмотреть файл

@@ -0,0 +1,9 @@
<?php

namespace Framework\Email\Exception;

use InvalidArgumentException;

class CompositionException extends InvalidArgumentException
{
}

+ 9
- 0
framework/Email/Exception/DriverException.php Просмотреть файл

@@ -0,0 +1,9 @@
<?php

namespace Framework\Email\Exception;

use RuntimeException;

class DriverException extends RuntimeException
{
}

+ 34
- 0
framework/Email/Factory.php Просмотреть файл

@@ -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');
}
}

+ 44
- 0
framework/Filesystem/Driver/Driver.php Просмотреть файл

@@ -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;
}
}

+ 16
- 0
framework/Filesystem/Driver/LocalDriver.php Просмотреть файл

@@ -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);
}
}

+ 9
- 0
framework/Filesystem/Exception/DriverException.php Просмотреть файл

@@ -0,0 +1,9 @@
<?php

namespace Framework\Filesystem\Exception;

use RuntimeException;

class DriverException extends RuntimeException
{
}

+ 34
- 0
framework/Filesystem/Factory.php Просмотреть файл

@@ -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');
}
}

+ 104
- 0
framework/Http/Response.php Просмотреть файл

@@ -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");
}
}

+ 10
- 0
framework/Logging/Driver/Driver.php Просмотреть файл

@@ -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;
}

+ 44
- 0
framework/Logging/Driver/StreamDriver.php Просмотреть файл

@@ -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;
}
}

+ 9
- 0
framework/Logging/Exception/DriverException.php Просмотреть файл

@@ -0,0 +1,9 @@
<?php

namespace Framework\Logging\Exception;

use RuntimeException;

class DriverException extends RuntimeException
{
}

+ 34
- 0
framework/Logging/Factory.php Просмотреть файл

@@ -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');
}
}

+ 38
- 0
framework/Provider/CacheProvider.php Просмотреть файл

@@ -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);
},
];
}
}

+ 16
- 0
framework/Provider/ConfigProvider.php Просмотреть файл

@@ -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();
});
}
}

+ 34
- 0
framework/Provider/DatabaseProvider.php Просмотреть файл

@@ -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);
},
];
}
}

+ 30
- 0
framework/Provider/EmailProvider.php Просмотреть файл

@@ -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);
},
];
}
}

+ 30
- 0
framework/Provider/FilesystemProvider.php Просмотреть файл

@@ -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);
},
];
}
}

+ 30
- 0
framework/Provider/LoggingProvider.php Просмотреть файл

@@ -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);
},
];
}
}

+ 30
- 0
framework/Provider/QueueProvider.php Просмотреть файл

@@ -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);
},
];
}
}

+ 16
- 0
framework/Provider/ResponseProvider.php Просмотреть файл

@@ -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();
});
}
}

+ 30
- 0
framework/Provider/SessionProvider.php Просмотреть файл

@@ -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.