From cc7c328f7e4a69c6e2103aad9fe73d8e82075d28 Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Mon, 20 Jul 2026 19:50:13 -0400 Subject: [PATCH] init --- .env.example | 13 + .gitignore | 5 + Dockerfile | 23 + app/Exceptions/Handler.php | 16 + .../Products/OrderProductController.php | 19 + .../Products/ShowProductController.php | 22 + .../Controllers/ShowHomePageController.php | 46 + .../Controllers/Users/LogInUserController.php | 27 + .../Users/LogOutUserController.php | 15 + .../Users/RegisterUserController.php | 34 + .../Users/ShowRegisterFormController.php | 17 + app/Models/Order.php | 15 + app/Models/Product.php | 27 + app/Models/Profile.php | 15 + app/Models/User.php | 20 + app/commands.php | 11 + app/routes.php | 51 + command.php | 17 + composer.json | 36 + composer.lock | 4886 +++++++++++++++++ config/cache.php | 19 + config/database.php | 17 + config/email.php | 13 + config/filesystem.php | 24 + config/handlers.php | 5 + config/logging.php | 11 + config/providers.php | 17 + config/queue.php | 10 + config/session.php | 9 + database/migrations/001_CreateOrdersTable.php | 18 + .../002_AddDeliveryInstructions.php | 13 + database/migrations/003_ChangeQuantity.php | 13 + database/migrations/004_DropPrice.php | 13 + .../migrations/005_CreateProductsTable.php | 15 + database/migrations/006_SeedProducts.php | 31 + database/migrations/007_CreateUsersTable.php | 16 + .../migrations/008_CreateProfilesTable.php | 14 + database/migrations/009_AddUserId.php | 13 + database/migrations/010_CreateJobsTable.php | 17 + docker-compose.yml | 41 + docker/nginx/default.conf | 23 + docker/php/app.ini | 3 + docker/start-container.sh | 53 + framework/App.php | 78 + framework/Cache/Driver/Driver.php | 31 + framework/Cache/Driver/FileDriver.php | 111 + framework/Cache/Driver/MemcacheDriver.php | 55 + framework/Cache/Driver/MemoryDriver.php | 54 + framework/Cache/Exception/DriverException.php | 9 + framework/Cache/Factory.php | 34 + framework/Container.php | 80 + framework/Database/Command/MigrateCommand.php | 100 + framework/Database/Connection/Connection.php | 45 + .../Database/Connection/MysqlConnection.php | 94 + .../Database/Connection/SqliteConnection.php | 69 + .../Exception/ConnectionException.php | 9 + .../Database/Exception/MigrationException.php | 9 + .../Database/Exception/QueryException.php | 9 + framework/Database/Factory.php | 34 + .../Database/Migration/Field/BoolField.php | 14 + .../Migration/Field/DateTimeField.php | 14 + framework/Database/Migration/Field/Field.php | 27 + .../Database/Migration/Field/FloatField.php | 14 + .../Database/Migration/Field/IdField.php | 13 + .../Database/Migration/Field/IntField.php | 14 + .../Database/Migration/Field/StringField.php | 14 + .../Database/Migration/Field/TextField.php | 21 + framework/Database/Migration/Migration.php | 62 + .../Database/Migration/MysqlMigration.php | 164 + .../Database/Migration/SqliteMigration.php | 154 + framework/Database/Model.php | 189 + framework/Database/ModelCollector.php | 56 + .../QueryBuilder/MysqlQueryBuilder.php | 15 + .../Database/QueryBuilder/QueryBuilder.php | 307 ++ .../QueryBuilder/SqliteQueryBuilder.php | 15 + framework/Database/Relationship.php | 27 + framework/Database/TableName.php | 14 + framework/Email/Driver/Driver.php | 12 + framework/Email/Driver/PostmarkDriver.php | 93 + .../Email/Exception/CompositionException.php | 9 + framework/Email/Exception/DriverException.php | 9 + framework/Email/Factory.php | 34 + framework/Filesystem/Driver/Driver.php | 44 + framework/Filesystem/Driver/LocalDriver.php | 16 + .../Filesystem/Exception/DriverException.php | 9 + framework/Filesystem/Factory.php | 34 + framework/Http/Response.php | 104 + framework/Logging/Driver/Driver.php | 10 + framework/Logging/Driver/StreamDriver.php | 44 + .../Logging/Exception/DriverException.php | 9 + framework/Logging/Factory.php | 34 + framework/Provider/CacheProvider.php | 38 + framework/Provider/ConfigProvider.php | 16 + framework/Provider/DatabaseProvider.php | 34 + framework/Provider/EmailProvider.php | 30 + framework/Provider/FilesystemProvider.php | 30 + framework/Provider/LoggingProvider.php | 30 + framework/Provider/QueueProvider.php | 30 + framework/Provider/ResponseProvider.php | 16 + framework/Provider/SessionProvider.php | 30 + framework/Provider/ValidationProvider.php | 34 + framework/Provider/ViewProvider.php | 51 + framework/Queue/Command/WorkCommand.php | 49 + framework/Queue/Driver/DatabaseDriver.php | 32 + framework/Queue/Driver/Driver.php | 12 + framework/Queue/Exception/DriverException.php | 9 + framework/Queue/Factory.php | 34 + framework/Queue/Job.php | 21 + .../Routing/Exception/RouteException.php | 9 + framework/Routing/Route.php | 123 + framework/Routing/Router.php | 131 + framework/Session/Driver/Driver.php | 31 + framework/Session/Driver/NativeDriver.php | 61 + .../Session/Exception/DriverException.php | 9 + framework/Session/Factory.php | 34 + framework/Support/Command/ServeCommand.php | 80 + framework/Support/Config.php | 44 + framework/Support/DriverFactory.php | 11 + framework/Support/DriverProvider.php | 29 + framework/Support/ExceptionHandler.php | 40 + framework/Testing/ServerExtension.php | 68 + framework/Testing/TestCase.php | 32 + framework/Testing/TestResponse.php | 42 + .../Exception/ValidationException.php | 33 + framework/Validation/Manager.php | 57 + framework/Validation/Rule/EmailRule.php | 20 + framework/Validation/Rule/MinRule.php | 30 + framework/Validation/Rule/RequiredRule.php | 16 + framework/Validation/Rule/Rule.php | 9 + framework/View/Engine/AdvancedEngine.php | 106 + framework/View/Engine/BasicEngine.php | 24 + framework/View/Engine/Engine.php | 12 + framework/View/Engine/HasManager.php | 16 + framework/View/Engine/LiteralEngine.php | 16 + framework/View/Engine/PhpEngine.php | 47 + framework/View/Manager.php | 59 + framework/View/View.php | 19 + framework/helpers.php | 132 + phpunit.xml | 14 + public/index.php | 7 + readme.md | 13 + resources/images/rocket.svg | 125 + resources/views/home.advanced.php | 22 + .../views/includes/large-feature.advanced.php | 24 + .../views/includes/small-feature.advanced.php | 21 + resources/views/layout.advanced.php | 20 + resources/views/products/view.advanced.php | 45 + resources/views/users/register.advanced.php | 106 + server.php | 11 + storage/.gitignore | 1 + storage/app/.gitignore | 2 + storage/framework/cache/.gitignore | 2 + storage/framework/views/.gitignore | 2 + tests/BrowserTest.php | 39 + tests/RoutingTest.php | 32 + tests/ValidationTest.php | 80 + tests/bootstrap.php | 7 + 157 files changed, 10548 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 app/Exceptions/Handler.php create mode 100644 app/Http/Controllers/Products/OrderProductController.php create mode 100644 app/Http/Controllers/Products/ShowProductController.php create mode 100644 app/Http/Controllers/ShowHomePageController.php create mode 100644 app/Http/Controllers/Users/LogInUserController.php create mode 100644 app/Http/Controllers/Users/LogOutUserController.php create mode 100644 app/Http/Controllers/Users/RegisterUserController.php create mode 100644 app/Http/Controllers/Users/ShowRegisterFormController.php create mode 100644 app/Models/Order.php create mode 100644 app/Models/Product.php create mode 100644 app/Models/Profile.php create mode 100644 app/Models/User.php create mode 100644 app/commands.php create mode 100644 app/routes.php create mode 100644 command.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config/cache.php create mode 100644 config/database.php create mode 100644 config/email.php create mode 100644 config/filesystem.php create mode 100644 config/handlers.php create mode 100644 config/logging.php create mode 100644 config/providers.php create mode 100644 config/queue.php create mode 100644 config/session.php create mode 100644 database/migrations/001_CreateOrdersTable.php create mode 100644 database/migrations/002_AddDeliveryInstructions.php create mode 100644 database/migrations/003_ChangeQuantity.php create mode 100644 database/migrations/004_DropPrice.php create mode 100644 database/migrations/005_CreateProductsTable.php create mode 100644 database/migrations/006_SeedProducts.php create mode 100644 database/migrations/007_CreateUsersTable.php create mode 100644 database/migrations/008_CreateProfilesTable.php create mode 100644 database/migrations/009_AddUserId.php create mode 100644 database/migrations/010_CreateJobsTable.php create mode 100644 docker-compose.yml create mode 100644 docker/nginx/default.conf create mode 100644 docker/php/app.ini create mode 100644 docker/start-container.sh create mode 100644 framework/App.php create mode 100644 framework/Cache/Driver/Driver.php create mode 100644 framework/Cache/Driver/FileDriver.php create mode 100644 framework/Cache/Driver/MemcacheDriver.php create mode 100644 framework/Cache/Driver/MemoryDriver.php create mode 100644 framework/Cache/Exception/DriverException.php create mode 100644 framework/Cache/Factory.php create mode 100644 framework/Container.php create mode 100644 framework/Database/Command/MigrateCommand.php create mode 100644 framework/Database/Connection/Connection.php create mode 100644 framework/Database/Connection/MysqlConnection.php create mode 100644 framework/Database/Connection/SqliteConnection.php create mode 100644 framework/Database/Exception/ConnectionException.php create mode 100644 framework/Database/Exception/MigrationException.php create mode 100644 framework/Database/Exception/QueryException.php create mode 100644 framework/Database/Factory.php create mode 100644 framework/Database/Migration/Field/BoolField.php create mode 100644 framework/Database/Migration/Field/DateTimeField.php create mode 100644 framework/Database/Migration/Field/Field.php create mode 100644 framework/Database/Migration/Field/FloatField.php create mode 100644 framework/Database/Migration/Field/IdField.php create mode 100644 framework/Database/Migration/Field/IntField.php create mode 100644 framework/Database/Migration/Field/StringField.php create mode 100644 framework/Database/Migration/Field/TextField.php create mode 100644 framework/Database/Migration/Migration.php create mode 100644 framework/Database/Migration/MysqlMigration.php create mode 100644 framework/Database/Migration/SqliteMigration.php create mode 100644 framework/Database/Model.php create mode 100644 framework/Database/ModelCollector.php create mode 100644 framework/Database/QueryBuilder/MysqlQueryBuilder.php create mode 100644 framework/Database/QueryBuilder/QueryBuilder.php create mode 100644 framework/Database/QueryBuilder/SqliteQueryBuilder.php create mode 100644 framework/Database/Relationship.php create mode 100644 framework/Database/TableName.php create mode 100644 framework/Email/Driver/Driver.php create mode 100644 framework/Email/Driver/PostmarkDriver.php create mode 100644 framework/Email/Exception/CompositionException.php create mode 100644 framework/Email/Exception/DriverException.php create mode 100644 framework/Email/Factory.php create mode 100644 framework/Filesystem/Driver/Driver.php create mode 100644 framework/Filesystem/Driver/LocalDriver.php create mode 100644 framework/Filesystem/Exception/DriverException.php create mode 100644 framework/Filesystem/Factory.php create mode 100644 framework/Http/Response.php create mode 100644 framework/Logging/Driver/Driver.php create mode 100644 framework/Logging/Driver/StreamDriver.php create mode 100644 framework/Logging/Exception/DriverException.php create mode 100644 framework/Logging/Factory.php create mode 100644 framework/Provider/CacheProvider.php create mode 100644 framework/Provider/ConfigProvider.php create mode 100644 framework/Provider/DatabaseProvider.php create mode 100644 framework/Provider/EmailProvider.php create mode 100644 framework/Provider/FilesystemProvider.php create mode 100644 framework/Provider/LoggingProvider.php create mode 100644 framework/Provider/QueueProvider.php create mode 100644 framework/Provider/ResponseProvider.php create mode 100644 framework/Provider/SessionProvider.php create mode 100644 framework/Provider/ValidationProvider.php create mode 100644 framework/Provider/ViewProvider.php create mode 100644 framework/Queue/Command/WorkCommand.php create mode 100644 framework/Queue/Driver/DatabaseDriver.php create mode 100644 framework/Queue/Driver/Driver.php create mode 100644 framework/Queue/Exception/DriverException.php create mode 100644 framework/Queue/Factory.php create mode 100644 framework/Queue/Job.php create mode 100644 framework/Routing/Exception/RouteException.php create mode 100644 framework/Routing/Route.php create mode 100644 framework/Routing/Router.php create mode 100644 framework/Session/Driver/Driver.php create mode 100644 framework/Session/Driver/NativeDriver.php create mode 100644 framework/Session/Exception/DriverException.php create mode 100644 framework/Session/Factory.php create mode 100644 framework/Support/Command/ServeCommand.php create mode 100644 framework/Support/Config.php create mode 100644 framework/Support/DriverFactory.php create mode 100644 framework/Support/DriverProvider.php create mode 100644 framework/Support/ExceptionHandler.php create mode 100644 framework/Testing/ServerExtension.php create mode 100644 framework/Testing/TestCase.php create mode 100644 framework/Testing/TestResponse.php create mode 100644 framework/Validation/Exception/ValidationException.php create mode 100644 framework/Validation/Manager.php create mode 100644 framework/Validation/Rule/EmailRule.php create mode 100644 framework/Validation/Rule/MinRule.php create mode 100644 framework/Validation/Rule/RequiredRule.php create mode 100644 framework/Validation/Rule/Rule.php create mode 100644 framework/View/Engine/AdvancedEngine.php create mode 100644 framework/View/Engine/BasicEngine.php create mode 100644 framework/View/Engine/Engine.php create mode 100644 framework/View/Engine/HasManager.php create mode 100644 framework/View/Engine/LiteralEngine.php create mode 100644 framework/View/Engine/PhpEngine.php create mode 100644 framework/View/Manager.php create mode 100644 framework/View/View.php create mode 100644 framework/helpers.php create mode 100644 phpunit.xml create mode 100644 public/index.php create mode 100644 readme.md create mode 100644 resources/images/rocket.svg create mode 100644 resources/views/home.advanced.php create mode 100644 resources/views/includes/large-feature.advanced.php create mode 100644 resources/views/includes/small-feature.advanced.php create mode 100644 resources/views/layout.advanced.php create mode 100644 resources/views/products/view.advanced.php create mode 100644 resources/views/users/register.advanced.php create mode 100644 server.php create mode 100644 storage/.gitignore create mode 100644 storage/app/.gitignore create mode 100644 storage/framework/cache/.gitignore create mode 100644 storage/framework/views/.gitignore create mode 100644 tests/BrowserTest.php create mode 100644 tests/RoutingTest.php create mode 100644 tests/ValidationTest.php create mode 100644 tests/bootstrap.php diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fc22301 --- /dev/null +++ b/.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= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64a1fa0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +database/*.sqlite +drivers/* +vendor +*.cache diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..17c4568 --- /dev/null +++ b/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"] diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php new file mode 100644 index 0000000..0972882 --- /dev/null +++ b/app/Exceptions/Handler.php @@ -0,0 +1,16 @@ +route('show-home-page')); + } +} diff --git a/app/Http/Controllers/Products/ShowProductController.php b/app/Http/Controllers/Products/ShowProductController.php new file mode 100644 index 0000000..2f4ed3d --- /dev/null +++ b/app/Http/Controllers/Products/ShowProductController.php @@ -0,0 +1,22 @@ +current()->parameters(); + + $product = Product::find((int) $parameters['product']); + + return view('products/view', [ + 'product' => $product, + 'orderAction' => $router->route('order-product', ['product' => $product->id]), + 'csrf' => csrf(), + ]); + } +} diff --git a/app/Http/Controllers/ShowHomePageController.php b/app/Http/Controllers/ShowHomePageController.php new file mode 100644 index 0000000..2309977 --- /dev/null +++ b/app/Http/Controllers/ShowHomePageController.php @@ -0,0 +1,46 @@ +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, + ]); + } +} diff --git a/app/Http/Controllers/Users/LogInUserController.php b/app/Http/Controllers/Users/LogInUserController.php new file mode 100644 index 0000000..028500b --- /dev/null +++ b/app/Http/Controllers/Users/LogInUserController.php @@ -0,0 +1,27 @@ + ['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')); + } +} diff --git a/app/Http/Controllers/Users/LogOutUserController.php b/app/Http/Controllers/Users/LogOutUserController.php new file mode 100644 index 0000000..185f31d --- /dev/null +++ b/app/Http/Controllers/Users/LogOutUserController.php @@ -0,0 +1,15 @@ +forget('user_id'); + + return redirect($router->route('show-home-page')); + } +} diff --git a/app/Http/Controllers/Users/RegisterUserController.php b/app/Http/Controllers/Users/RegisterUserController.php new file mode 100644 index 0000000..d201f28 --- /dev/null +++ b/app/Http/Controllers/Users/RegisterUserController.php @@ -0,0 +1,34 @@ + ['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')); + } +} diff --git a/app/Http/Controllers/Users/ShowRegisterFormController.php b/app/Http/Controllers/Users/ShowRegisterFormController.php new file mode 100644 index 0000000..1bec47e --- /dev/null +++ b/app/Http/Controllers/Users/ShowRegisterFormController.php @@ -0,0 +1,17 @@ + $router->route('register-user'), + 'logInAction' => $router->route('log-in-user'), + 'csrf' => csrf(), + ]); + } +} diff --git a/app/Models/Order.php b/app/Models/Order.php new file mode 100644 index 0000000..1b24a6d --- /dev/null +++ b/app/Models/Order.php @@ -0,0 +1,15 @@ +belongsTo(User::class, 'user_id'); + } +} diff --git a/app/Models/Product.php b/app/Models/Product.php new file mode 100644 index 0000000..9baa7f0 --- /dev/null +++ b/app/Models/Product.php @@ -0,0 +1,27 @@ +belongsTo(User::class, 'user_id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..59596b1 --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,20 @@ +hasOne(Profile::class, 'user_id'); + } + + public function orders(): mixed + { + return $this->hasMany(Order::class, 'user_id'); + } +} diff --git a/app/commands.php b/app/commands.php new file mode 100644 index 0000000..e3d7b9d --- /dev/null +++ b/app/commands.php @@ -0,0 +1,11 @@ +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'); +}; diff --git a/command.php b/command.php new file mode 100644 index 0000000..2673110 --- /dev/null +++ b/command.php @@ -0,0 +1,17 @@ +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(); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2fe461c --- /dev/null +++ b/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" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..003e755 --- /dev/null +++ b/composer.lock @@ -0,0 +1,4886 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "106b52a748c01bbdd43c9758984317f3", + "packages": [ + { + "name": "doctrine/lexer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.11.8", + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2020-05-25T17:44:05+00:00" + }, + { + "name": "egulias/email-validator", + "version": "2.1.25", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/0dbf5d78455d4d6a41d186da50adc1122ec066f4", + "reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.0.1", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.10" + }, + "require-dev": { + "dominicsayers/isemail": "^3.0.7", + "phpunit/phpunit": "^4.8.36|^7.5.15", + "satooshi/php-coveralls": "^1.0.1" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/2.1.25" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2020-12-29T14:50:06+00:00" + }, + { + "name": "filp/whoops", + "version": "2.1.3", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "8828aaa2178e0a19325522e2a45282ff0a14649b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/8828aaa2178e0a19325522e2a45282ff0a14649b", + "reference": "8828aaa2178e0a19325522e2a45282ff0a14649b", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "mockery/mockery": "0.9.*", + "phpunit/phpunit": "^4.8 || ^5.0", + "symfony/var-dumper": "~3.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://github.com/filp/whoops", + "keywords": [ + "error", + "exception", + "handling", + "library", + "whoops", + "zf2" + ], + "time": "2016-05-06T18:25:35+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "7e279d2cd5d7fbb156ce46daada972355cea27bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/7e279d2cd5d7fbb156ce46daada972355cea27bb", + "reference": "7e279d2cd5d7fbb156ce46daada972355cea27bb", + "shasum": "" + }, + "require": { + "php": "^7.0|^8.0", + "phpoption/phpoption": "^1.7.3" + }, + "require-dev": { + "phpunit/phpunit": "^6.5|^7.5|^8.5|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "graham@alt-three.com" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2020-04-13T13:17:36+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/0aa74dfb41ae110835923ef10a9d803a22d50e79", + "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.4", + "guzzlehttp/psr7": "^1.7", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "ext-curl": "*", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "psr/log": "^1.1" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.1-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://github.com/alexeyshockov", + "type": "github" + }, + { + "url": "https://github.com/gmponos", + "type": "github" + } + ], + "time": "2020-10-10T11:47:56+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "60d379c243457e073cff02bc323a2a86cb355631" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631", + "reference": "60d379c243457e073cff02bc323a2a86cb355631", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.4.0" + }, + "time": "2020-09-30T07:37:28+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/53330f47520498c0ae1f61f7e2c90f55690c06a3", + "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.7.0" + }, + "time": "2020-09-30T07:37:11+00:00" + }, + { + "name": "league/flysystem", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "1d68c2325a56b6b847f3a40b9564cabfa7bb2594" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1d68c2325a56b6b847f3a40b9564cabfa7bb2594", + "reference": "1d68c2325a56b6b847f3a40b9564cabfa7bb2594", + "shasum": "" + }, + "require": { + "ext-json": "*", + "league/mime-type-detection": "^1.0.0", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "guzzlehttp/ringphp": "<1.1.1" + }, + "require-dev": { + "async-aws/s3": "^1.5", + "async-aws/simple-s3": "^1.0", + "aws/aws-sdk-php": "^3.132.4", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "friendsofphp/php-cs-fixer": "^2.16", + "google/cloud-storage": "^1.23", + "phpseclib/phpseclib": "^2.0", + "phpstan/phpstan": "^0.12.26", + "phpunit/phpunit": "^8.5 || ^9.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/2.0.2" + }, + "funding": [ + { + "url": "https://offset.earth/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2020-12-28T17:02:23+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3", + "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.18", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.7.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2021-01-18T20:58:21+00:00" + }, + { + "name": "monolog/monolog", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1cb1cde8e8dd0f70cc0fe51354a59acad9302084", + "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7", + "graylog2/gelf-php": "^1.4.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpspec/prophecy": "^1.6.1", + "phpstan/phpstan": "^0.12.59", + "phpunit/phpunit": "^8.5", + "predis/predis": "^1.1", + "rollbar/rollbar": "^1.3", + "ruflin/elastica": ">=0.90 <7.0.1", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2020-12-14T13:15:25+00:00" + }, + { + "name": "opis/closure", + "version": "3.6.1", + "source": { + "type": "git", + "url": "https://github.com/opis/closure.git", + "reference": "943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/closure/zipball/943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5", + "reference": "943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0" + }, + "require-dev": { + "jeremeamia/superclosure": "^2.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\Closure\\": "src/" + }, + "files": [ + "functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary objects.", + "homepage": "https://opis.io/closure", + "keywords": [ + "anonymous functions", + "closure", + "function", + "serializable", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/opis/closure/issues", + "source": "https://github.com/opis/closure/tree/3.6.1" + }, + "time": "2020-11-07T02:01:34+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.7.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/994ecccd8f3283ecf5ac33254543eb0ac946d525", + "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "phpunit/phpunit": "^4.8.35 || ^5.7.27 || ^6.5.6 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2020-07-20T17:29:33+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/master" + }, + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.3" + }, + "time": "2020-03-23T09:12:05+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "swiftmailer/swiftmailer", + "version": "v6.2.5", + "source": { + "type": "git", + "url": "https://github.com/swiftmailer/swiftmailer.git", + "reference": "698a6a9f54d7eb321274de3ad19863802c879fb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/698a6a9f54d7eb321274de3ad19863802c879fb7", + "reference": "698a6a9f54d7eb321274de3ad19863802c879fb7", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.0", + "php": ">=7.0.0", + "symfony/polyfill-iconv": "^1.0", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "symfony/phpunit-bridge": "^4.4|^5.0" + }, + "suggest": { + "ext-intl": "Needed to support internationalized email addresses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "lib/swift_required.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Corbyn" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Swiftmailer, free feature-rich PHP mailer", + "homepage": "https://swiftmailer.symfony.com", + "keywords": [ + "email", + "mail", + "mailer" + ], + "support": { + "issues": "https://github.com/swiftmailer/swiftmailer/issues", + "source": "https://github.com/swiftmailer/swiftmailer/tree/v6.2.5" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/swiftmailer/swiftmailer", + "type": "tidelift" + } + ], + "time": "2021-01-12T09:35:59+00:00" + }, + { + "name": "symfony/console", + "version": "v5.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e", + "reference": "e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.1|^2", + "symfony/string": "^5.1" + }, + "conflict": { + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/event-dispatcher": "^4.4|^5.0", + "symfony/lock": "^4.4|^5.0", + "symfony/process": "^4.4|^5.0", + "symfony/var-dumper": "^4.4|^5.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/console/tree/v5.1.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/polyfill-iconv", + "version": "v1.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-iconv.git", + "reference": "b34bfb8c4c22650ac080d2662ae3502e5f2f4ae6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/b34bfb8c4c22650ac080d2662ae3502e5f2f4ae6", + "reference": "b34bfb8c4c22650ac080d2662ae3502e5f2f4ae6", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-iconv": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Iconv\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Iconv extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "iconv", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.22.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c", + "reference": "c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.20.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44", + "reference": "0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.22.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "727d1096295d807c309fb01a851577302394c897" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897", + "reference": "727d1096295d807c309fb01a851577302394c897", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.20.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a", + "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.22.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/8ff431c517be11c78c48a39a66d37431e26a6bed", + "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.20.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "shasum": "" + }, + "require": { + "php": ">=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.0" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/master" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-09-07T11:33:47+00:00" + }, + { + "name": "symfony/string", + "version": "v5.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "a97573e960303db71be0dd8fda9be3bca5e0feea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/a97573e960303db71be0dd8fda9be3bca5e0feea", + "reference": "a97573e960303db71be0dd8fda9be3bca5e0feea", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "files": [ + "Resources/functions.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony String component", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.1.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.2.0", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "fba64139db67123c7a57072e5f8d3db10d160b66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/fba64139db67123c7a57072e5f8d3db10d160b66", + "reference": "fba64139db67123c7a57072e5f8d3db10d160b66", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.0.1", + "php": "^7.1.3 || ^8.0", + "phpoption/phpoption": "^1.7.4", + "symfony/polyfill-ctype": "^1.17", + "symfony/polyfill-mbstring": "^1.17", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-filter": "*", + "phpunit/phpunit": "^7.5.20 || ^8.5.2 || ^9.0" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.2-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "homepage": "https://gjcampbell.co.uk/" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://vancelucas.com/" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2020-09-14T15:57:31+00:00" + }, + { + "name": "wildbit/swiftmailer-postmark", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/wildbit/swiftmailer-postmark.git", + "reference": "44ccab7834de8b220d292647ecb2cb683f9962ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wildbit/swiftmailer-postmark/zipball/44ccab7834de8b220d292647ecb2cb683f9962ee", + "reference": "44ccab7834de8b220d292647ecb2cb683f9962ee", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0|^7.0", + "swiftmailer/swiftmailer": "^6.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Postmark\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Postmark", + "email": "support@postmarkapp.com" + } + ], + "description": "A Swiftmailer Transport for Postmark.", + "support": { + "issues": "https://github.com/wildbit/swiftmailer-postmark/issues", + "source": "https://github.com/wildbit/swiftmailer-postmark/tree/3.3.0" + }, + "time": "2020-09-10T10:54:20+00:00" + } + ], + "packages-dev": [ + { + "name": "dbrekelmans/bdi", + "version": "0.3", + "source": { + "type": "git", + "url": "https://github.com/dbrekelmans/bdi.git", + "reference": "d94d818ebe20b419a7d9f17dc21e9c66a10b171f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dbrekelmans/bdi/zipball/d94d818ebe20b419a7d9f17dc21e9c66a10b171f", + "reference": "d94d818ebe20b419a7d9f17dc21e9c66a10b171f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-zip": "*", + "ext-zlib": "*", + "php": "^7.2|^8.0" + }, + "bin": [ + "bdi", + "bdi.phar" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniël Brekelmans", + "homepage": "https://github.com/dbrekelmans" + }, + { + "name": "Contributors", + "homepage": "https://github.com/dbrekelmans/bdi/graphs/contributors" + } + ], + "description": "PHAR distribution of dbrekelmans/browser-driver-installer.", + "homepage": "https://github.com/dbrekelmans/bdi", + "keywords": [ + "browser-driver-installer" + ], + "support": { + "source": "https://github.com/dbrekelmans/bdi/tree/0.3" + }, + "time": "2020-12-13T21:43:40+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-11-10T18:47:58+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.10.2", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-11-13T09:40:50+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.10.4", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c6d052fc58cb876152f89f532b95a8d7907e7f0e", + "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.4" + }, + "time": "2020-12-20T10:01:03+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/master" + }, + "time": "2020-06-27T14:33:11+00:00" + }, + { + "name": "phar-io/version", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "e4782611070e50613683d2b9a57730e9a3ba5451" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/e4782611070e50613683d2b9a57730e9a3ba5451", + "reference": "e4782611070e50613683d2b9a57730e9a3ba5451", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.0.4" + }, + "time": "2020-12-13T23:18:30+00:00" + }, + { + "name": "php-webdriver/webdriver", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/php-webdriver/php-webdriver.git", + "reference": "e3633154554605274cc9d59837f55a7427d72003" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/e3633154554605274cc9d59837f55a7427d72003", + "reference": "e3633154554605274cc9d59837f55a7427d72003", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-zip": "*", + "php": "^5.6 || ~7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.12", + "symfony/process": "^2.8 || ^3.1 || ^4.0 || ^5.0" + }, + "replace": { + "facebook/webdriver": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.0", + "ondram/ci-detector": "^2.1 || ^3.5", + "php-coveralls/php-coveralls": "^2.4", + "php-mock/php-mock-phpunit": "^1.1 || ^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpunit/phpunit": "^5.7 || ^7 || ^8 || ^9", + "squizlabs/php_codesniffer": "^3.5", + "symfony/var-dumper": "^3.3 || ^4.0 || ^5.0" + }, + "suggest": { + "ext-SimpleXML": "For Firefox profile creation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + }, + "files": [ + "lib/Exception/TimeoutException.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", + "homepage": "https://github.com/php-webdriver/php-webdriver", + "keywords": [ + "Chromedriver", + "geckodriver", + "php", + "selenium", + "webdriver" + ], + "support": { + "issues": "https://github.com/php-webdriver/php-webdriver/issues", + "source": "https://github.com/php-webdriver/php-webdriver/tree/1.9.0" + }, + "time": "2020-11-19T15:21:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.2.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + }, + "time": "2020-09-03T19:13:55+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + }, + "time": "2020-09-17T18:55:26+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.12.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "245710e971a030f42e08f4912863805570f23d39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/245710e971a030f42e08f4912863805570f23d39", + "reference": "245710e971a030f42e08f4912863805570f23d39", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2 || ~8.0, <8.1", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/1.12.2" + }, + "time": "2020-12-19T10:15:11+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f3e026641cc91909d421802dd3ac7827ebfd97e1", + "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.10.2", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:44:49+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", + "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:57:25+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.5.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "8e16c225d57c3d6808014df6b1dd7598d0a5bbbe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8e16c225d57c3d6808014df6b1dd7598d0a5bbbe", + "reference": "8e16c225d57c3d6808014df6b1dd7598d0a5bbbe", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.1", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpspec/prophecy": "^1.12.1", + "phpunit/php-code-coverage": "^9.2.3", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.5", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.3", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^2.3", + "sebastian/version": "^3.0.2" + }, + "require-dev": { + "ext-pdo": "*", + "phpspec/prophecy-phpunit": "^2.0.1" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ], + "files": [ + "src/Framework/Assert/Functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.0" + }, + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-12-04T05:05:53+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:49:45+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:10:38+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", + "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:52:38+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:24:23+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "a90ccbddffa067b51f574dea6eb25d5680839455" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/a90ccbddffa067b51f574dea6eb25d5680839455", + "reference": "a90ccbddffa067b51f574dea6eb25d5680839455", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:55:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:17:30+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "2.3.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/2.3.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:18:59+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "symfony/browser-kit", + "version": "v5.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "87d6f0a7436b03a57d4cf9a6a9cd0c83a355c49a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/87d6f0a7436b03a57d4cf9a6a9cd0c83a355c49a", + "reference": "87d6f0a7436b03a57d4cf9a6a9cd0c83a355c49a", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/dom-crawler": "^4.4|^5.0" + }, + "require-dev": { + "symfony/css-selector": "^4.4|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/mime": "^4.4|^5.0", + "symfony/process": "^4.4|^5.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony BrowserKit Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/browser-kit/tree/v5.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-12-18T08:03:05+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v5.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "ee7cf316fb0de786cfe5ae32ee79502b290c81ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/ee7cf316fb0de786cfe5ae32ee79502b290c81ea", + "reference": "ee7cf316fb0de786cfe5ae32ee79502b290c81ea", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "masterminds/html5": "<2.6" + }, + "require-dev": { + "masterminds/html5": "^2.6", + "symfony/css-selector": "^4.4|^5.0" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DomCrawler Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v5.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-12-18T08:02:46+00:00" + }, + { + "name": "symfony/http-client", + "version": "v5.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "a77cbec69ea90dea509beef29b79748c0df33a83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/a77cbec69ea90dea509beef29b79748c0df33a83", + "reference": "a77cbec69ea90dea509beef29b79748c0df33a83", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/log": "^1.0", + "symfony/http-client-contracts": "^2.2", + "symfony/polyfill-php73": "^1.11", + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.0|^2" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "1.1" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.3.1", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/http-kernel": "^4.4.13|^5.1.5", + "symfony/process": "^4.4|^5.0", + "symfony/stopwatch": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpClient component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-client/tree/v5.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-12-14T10:56:50+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "41db680a15018f9c1d4b23516059633ce280ca33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41db680a15018f9c1d4b23516059633ce280ca33", + "reference": "41db680a15018f9c1d4b23516059633ce280ca33", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "symfony/http-client-implementation": "" + }, + "type": "library", + "extra": { + "branch-version": "2.3", + "branch-alias": { + "dev-main": "2.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-14T17:08:19+00:00" + }, + { + "name": "symfony/panther", + "version": "v0.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/panther.git", + "reference": "dc23945f2aab40c67e45f50c1486a16dd03423f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/panther/zipball/dc23945f2aab40c67e45f50c1486a16dd03423f2", + "reference": "dc23945f2aab40c67e45f50c1486a16dd03423f2", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "php-webdriver/webdriver": "^1.8.2", + "symfony/browser-kit": "^4.4 || ^5.0", + "symfony/dom-crawler": "^4.4 || ^5.0", + "symfony/http-client": "^4.4.11 || ^5.2", + "symfony/polyfill-php72": "^1.9", + "symfony/process": "^4.4 || ^5.0" + }, + "require-dev": { + "fabpot/goutte": "^3.2.3", + "guzzlehttp/guzzle": "^6.3", + "symfony/css-selector": "^4.4 || ^5.0", + "symfony/framework-bundle": "^4.4 || ^5.0", + "symfony/mime": "^4.4 || ^5.0", + "symfony/phpunit-bridge": "^5.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Panther\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com", + "homepage": "https://dunglas.fr" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A browser testing and web scraping library for PHP and Symfony.", + "homepage": "https://dunglas.fr", + "keywords": [ + "e2e", + "scraping", + "selenium", + "symfony", + "testing", + "webdriver" + ], + "support": { + "issues": "https://github.com/symfony/panther/issues", + "source": "https://github.com/symfony/panther/tree/v0.9.0" + }, + "funding": [ + { + "url": "https://www.panthera.org/donate", + "type": "custom" + } + ], + "time": "2020-12-29T07:51:42+00:00" + }, + { + "name": "symfony/process", + "version": "v5.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "bd8815b8b6705298beaa384f04fabd459c10bedd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/bd8815b8b6705298beaa384f04fabd459c10bedd", + "reference": "bd8815b8b6705298beaa384f04fabd459c10bedd", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.15" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v5.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-12-08T17:03:37+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "75a63c33a8577608444246075ea0af0d052e452a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", + "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/master" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2020-07-12T23:59:07+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<3.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozart/assert/issues", + "source": "https://github.com/webmozart/assert/tree/master" + }, + "time": "2020-07-08T17:02:28+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..dfcf75c --- /dev/null +++ b/config/cache.php @@ -0,0 +1,19 @@ + 'memory', + 'memory' => [ + 'type' => 'memory', + 'seconds' => 31536000, + ], + 'file' => [ + 'type' => 'file', + 'seconds' => 31536000, + ], + 'memcache' => [ + 'type' => 'memcache', + 'host' => '127.0.0.1', + 'port' => 11211, + 'seconds' => 31536000, + ], +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..956df41 --- /dev/null +++ b/config/database.php @@ -0,0 +1,17 @@ + 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'), + ], +]; diff --git a/config/email.php b/config/email.php new file mode 100644 index 0000000..710671e --- /dev/null +++ b/config/email.php @@ -0,0 +1,13 @@ + 'postmark', + 'postmark' => [ + 'type' => 'postmark', + 'token' => env('EMAIL_TOKEN'), + 'from' => [ + 'name' => env('EMAIL_FROM_NAME'), + 'email' => env('EMAIL_FROM_EMAIL'), + ], + ] +]; diff --git a/config/filesystem.php b/config/filesystem.php new file mode 100644 index 0000000..f627d00 --- /dev/null +++ b/config/filesystem.php @@ -0,0 +1,24 @@ + 'local', + 'local' => [ + 'type' => 'local', + 'path' => __DIR__ . '/../storage/app', + ], + 's3' => [ + 'type' => 's3', + 'key' => '', + 'secret' => '', + 'token' => '', + 'region' => '', + 'bucket' => '', + ], + 'ftp' => [ + 'type' => 'ftp', + 'host' => '', + 'root' => '', + 'username' => '', + 'password' => '', + ], +]; diff --git a/config/handlers.php b/config/handlers.php new file mode 100644 index 0000000..31c92b9 --- /dev/null +++ b/config/handlers.php @@ -0,0 +1,5 @@ + \App\Exceptions\Handler::class, +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..cb917db --- /dev/null +++ b/config/logging.php @@ -0,0 +1,11 @@ + 'stream', + 'stream' => [ + 'type' => 'stream', + 'path' => __DIR__ . '/../storage/app.log', + 'name' => 'App', + 'minimum' => \Monolog\Logger::DEBUG, + ], +]; diff --git a/config/providers.php b/config/providers.php new file mode 100644 index 0000000..1673c5b --- /dev/null +++ b/config/providers.php @@ -0,0 +1,17 @@ + 'database', + 'database' => [ + 'type' => 'database', + 'table' => 'jobs', + 'attempts' => 3, + ], +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..7000e27 --- /dev/null +++ b/config/session.php @@ -0,0 +1,9 @@ + 'native', + 'native' => [ + 'type' => 'native', + 'prefix' => 'framework_', + ], +]; diff --git a/database/migrations/001_CreateOrdersTable.php b/database/migrations/001_CreateOrdersTable.php new file mode 100644 index 0000000..fd30ecb --- /dev/null +++ b/database/migrations/001_CreateOrdersTable.php @@ -0,0 +1,18 @@ +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(); + } +} diff --git a/database/migrations/002_AddDeliveryInstructions.php b/database/migrations/002_AddDeliveryInstructions.php new file mode 100644 index 0000000..d908332 --- /dev/null +++ b/database/migrations/002_AddDeliveryInstructions.php @@ -0,0 +1,13 @@ +alterTable('orders'); + $table->text('delivery_instructions'); + $table->execute(); + } +} diff --git a/database/migrations/003_ChangeQuantity.php b/database/migrations/003_ChangeQuantity.php new file mode 100644 index 0000000..56fa336 --- /dev/null +++ b/database/migrations/003_ChangeQuantity.php @@ -0,0 +1,13 @@ +alterTable('orders'); + $table->int('quantity')->nullable()->alter(); + $table->execute(); + } +} diff --git a/database/migrations/004_DropPrice.php b/database/migrations/004_DropPrice.php new file mode 100644 index 0000000..7a4f8aa --- /dev/null +++ b/database/migrations/004_DropPrice.php @@ -0,0 +1,13 @@ +alterTable('orders'); + $table->dropColumn('price'); + $table->execute(); + } +} diff --git a/database/migrations/005_CreateProductsTable.php b/database/migrations/005_CreateProductsTable.php new file mode 100644 index 0000000..61e226c --- /dev/null +++ b/database/migrations/005_CreateProductsTable.php @@ -0,0 +1,15 @@ +createTable('products'); + $table->id('id'); + $table->string('name'); + $table->text('description'); + $table->execute(); + } +} diff --git a/database/migrations/006_SeedProducts.php b/database/migrations/006_SeedProducts.php new file mode 100644 index 0000000..579fc63 --- /dev/null +++ b/database/migrations/006_SeedProducts.php @@ -0,0 +1,31 @@ + '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); + } + } +} diff --git a/database/migrations/007_CreateUsersTable.php b/database/migrations/007_CreateUsersTable.php new file mode 100644 index 0000000..50ce7d3 --- /dev/null +++ b/database/migrations/007_CreateUsersTable.php @@ -0,0 +1,16 @@ +createTable('users'); + $table->id('id'); + $table->string('name'); + $table->string('email'); + $table->string('password'); + $table->execute(); + } +} diff --git a/database/migrations/008_CreateProfilesTable.php b/database/migrations/008_CreateProfilesTable.php new file mode 100644 index 0000000..9738ab9 --- /dev/null +++ b/database/migrations/008_CreateProfilesTable.php @@ -0,0 +1,14 @@ +createTable('profiles'); + $table->id('id'); + $table->int('user_id'); + $table->execute(); + } +} diff --git a/database/migrations/009_AddUserId.php b/database/migrations/009_AddUserId.php new file mode 100644 index 0000000..b34895d --- /dev/null +++ b/database/migrations/009_AddUserId.php @@ -0,0 +1,13 @@ +alterTable('orders'); + $table->int('user_id'); + $table->execute(); + } +} diff --git a/database/migrations/010_CreateJobsTable.php b/database/migrations/010_CreateJobsTable.php new file mode 100644 index 0000000..7fc4844 --- /dev/null +++ b/database/migrations/010_CreateJobsTable.php @@ -0,0 +1,17 @@ +createTable('jobs'); + $table->id('id'); + $table->text('closure'); + $table->text('params'); + $table->int('attempts')->default(0); + $table->bool('is_complete')->default(false); + $table->execute(); + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..adf8f0c --- /dev/null +++ b/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: diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..162ae38 --- /dev/null +++ b/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; + } +} diff --git a/docker/php/app.ini b/docker/php/app.ini new file mode 100644 index 0000000..4fe9d65 --- /dev/null +++ b/docker/php/app.ini @@ -0,0 +1,3 @@ +error_reporting = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED +display_errors = On +log_errors = On diff --git a/docker/start-container.sh b/docker/start-container.sh new file mode 100644 index 0000000..e4e337a --- /dev/null +++ b/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;" diff --git a/framework/App.php b/framework/App.php new file mode 100644 index 0000000..b8e202e --- /dev/null +++ b/framework/App.php @@ -0,0 +1,78 @@ +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; + } +} diff --git a/framework/Cache/Driver/Driver.php b/framework/Cache/Driver/Driver.php new file mode 100644 index 0000000..2e899e3 --- /dev/null +++ b/framework/Cache/Driver/Driver.php @@ -0,0 +1,31 @@ +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; + } +} diff --git a/framework/Cache/Driver/MemcacheDriver.php b/framework/Cache/Driver/MemcacheDriver.php new file mode 100644 index 0000000..2f989d4 --- /dev/null +++ b/framework/Cache/Driver/MemcacheDriver.php @@ -0,0 +1,55 @@ +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; + } +} diff --git a/framework/Cache/Driver/MemoryDriver.php b/framework/Cache/Driver/MemoryDriver.php new file mode 100644 index 0000000..dfbe8f2 --- /dev/null +++ b/framework/Cache/Driver/MemoryDriver.php @@ -0,0 +1,54 @@ +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; + } +} diff --git a/framework/Cache/Exception/DriverException.php b/framework/Cache/Exception/DriverException.php new file mode 100644 index 0000000..cc4ffa4 --- /dev/null +++ b/framework/Cache/Exception/DriverException.php @@ -0,0 +1,9 @@ +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'); + } +} diff --git a/framework/Container.php b/framework/Container.php new file mode 100644 index 0000000..f777a62 --- /dev/null +++ b/framework/Container.php @@ -0,0 +1,80 @@ +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); + } +} diff --git a/framework/Database/Command/MigrateCommand.php b/framework/Database/Command/MigrateCommand.php new file mode 100644 index 0000000..6ed4e96 --- /dev/null +++ b/framework/Database/Command/MigrateCommand.php @@ -0,0 +1,100 @@ +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(); + } +} diff --git a/framework/Database/Connection/Connection.php b/framework/Database/Connection/Connection.php new file mode 100644 index 0000000..5871cef --- /dev/null +++ b/framework/Database/Connection/Connection.php @@ -0,0 +1,45 @@ + $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(); + } +} diff --git a/framework/Database/Connection/SqliteConnection.php b/framework/Database/Connection/SqliteConnection.php new file mode 100644 index 0000000..a50cf0f --- /dev/null +++ b/framework/Database/Connection/SqliteConnection.php @@ -0,0 +1,69 @@ + $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; + } +} diff --git a/framework/Database/Exception/ConnectionException.php b/framework/Database/Exception/ConnectionException.php new file mode 100644 index 0000000..19c8189 --- /dev/null +++ b/framework/Database/Exception/ConnectionException.php @@ -0,0 +1,9 @@ +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'); + } +} diff --git a/framework/Database/Migration/Field/BoolField.php b/framework/Database/Migration/Field/BoolField.php new file mode 100644 index 0000000..c9f1f5f --- /dev/null +++ b/framework/Database/Migration/Field/BoolField.php @@ -0,0 +1,14 @@ +default = $value; + return $this; + } +} diff --git a/framework/Database/Migration/Field/DateTimeField.php b/framework/Database/Migration/Field/DateTimeField.php new file mode 100644 index 0000000..72784d5 --- /dev/null +++ b/framework/Database/Migration/Field/DateTimeField.php @@ -0,0 +1,14 @@ +default = $value; + return $this; + } +} diff --git a/framework/Database/Migration/Field/Field.php b/framework/Database/Migration/Field/Field.php new file mode 100644 index 0000000..f5bd007 --- /dev/null +++ b/framework/Database/Migration/Field/Field.php @@ -0,0 +1,27 @@ +name = $name; + } + + public function nullable(): static + { + $this->nullable = true; + return $this; + } + + public function alter(): static + { + $this->alter = true; + return $this; + } +} diff --git a/framework/Database/Migration/Field/FloatField.php b/framework/Database/Migration/Field/FloatField.php new file mode 100644 index 0000000..e1798ac --- /dev/null +++ b/framework/Database/Migration/Field/FloatField.php @@ -0,0 +1,14 @@ +default = $value; + return $this; + } +} diff --git a/framework/Database/Migration/Field/IdField.php b/framework/Database/Migration/Field/IdField.php new file mode 100644 index 0000000..aa4007e --- /dev/null +++ b/framework/Database/Migration/Field/IdField.php @@ -0,0 +1,13 @@ +default = $value; + return $this; + } +} diff --git a/framework/Database/Migration/Field/StringField.php b/framework/Database/Migration/Field/StringField.php new file mode 100644 index 0000000..e6f75be --- /dev/null +++ b/framework/Database/Migration/Field/StringField.php @@ -0,0 +1,14 @@ +default = $value; + return $this; + } +} diff --git a/framework/Database/Migration/Field/TextField.php b/framework/Database/Migration/Field/TextField.php new file mode 100644 index 0000000..e102f82 --- /dev/null +++ b/framework/Database/Migration/Field/TextField.php @@ -0,0 +1,21 @@ +default = $value; + return $this; + } +} diff --git a/framework/Database/Migration/Migration.php b/framework/Database/Migration/Migration.php new file mode 100644 index 0000000..8c1cf24 --- /dev/null +++ b/framework/Database/Migration/Migration.php @@ -0,0 +1,62 @@ +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; +} diff --git a/framework/Database/Migration/MysqlMigration.php b/framework/Database/Migration/MysqlMigration.php new file mode 100644 index 0000000..755052e --- /dev/null +++ b/framework/Database/Migration/MysqlMigration.php @@ -0,0 +1,164 @@ +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; + } +} diff --git a/framework/Database/Migration/SqliteMigration.php b/framework/Database/Migration/SqliteMigration.php new file mode 100644 index 0000000..765704d --- /dev/null +++ b/framework/Database/Migration/SqliteMigration.php @@ -0,0 +1,154 @@ +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'); + } +} diff --git a/framework/Database/Model.php b/framework/Database/Model.php new file mode 100644 index 0000000..b6fde84 --- /dev/null +++ b/framework/Database/Model.php @@ -0,0 +1,189 @@ +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(); + } +} diff --git a/framework/Database/ModelCollector.php b/framework/Database/ModelCollector.php new file mode 100644 index 0000000..86dcdf2 --- /dev/null +++ b/framework/Database/ModelCollector.php @@ -0,0 +1,56 @@ +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; + } +} diff --git a/framework/Database/QueryBuilder/MysqlQueryBuilder.php b/framework/Database/QueryBuilder/MysqlQueryBuilder.php new file mode 100644 index 0000000..266855c --- /dev/null +++ b/framework/Database/QueryBuilder/MysqlQueryBuilder.php @@ -0,0 +1,15 @@ +connection = $connection; + } +} diff --git a/framework/Database/QueryBuilder/QueryBuilder.php b/framework/Database/QueryBuilder/QueryBuilder.php new file mode 100644 index 0000000..8f73db3 --- /dev/null +++ b/framework/Database/QueryBuilder/QueryBuilder.php @@ -0,0 +1,307 @@ +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()); + } +} diff --git a/framework/Database/QueryBuilder/SqliteQueryBuilder.php b/framework/Database/QueryBuilder/SqliteQueryBuilder.php new file mode 100644 index 0000000..f8d7a79 --- /dev/null +++ b/framework/Database/QueryBuilder/SqliteQueryBuilder.php @@ -0,0 +1,15 @@ +connection = $connection; + } +} diff --git a/framework/Database/Relationship.php b/framework/Database/Relationship.php new file mode 100644 index 0000000..b50df6c --- /dev/null +++ b/framework/Database/Relationship.php @@ -0,0 +1,27 @@ +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); + } +} diff --git a/framework/Database/TableName.php b/framework/Database/TableName.php new file mode 100644 index 0000000..983d173 --- /dev/null +++ b/framework/Database/TableName.php @@ -0,0 +1,14 @@ +name = $name; + } +} diff --git a/framework/Email/Driver/Driver.php b/framework/Email/Driver/Driver.php new file mode 100644 index 0000000..979201a --- /dev/null +++ b/framework/Email/Driver/Driver.php @@ -0,0 +1,12 @@ +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; + } +} diff --git a/framework/Email/Exception/CompositionException.php b/framework/Email/Exception/CompositionException.php new file mode 100644 index 0000000..7914b5a --- /dev/null +++ b/framework/Email/Exception/CompositionException.php @@ -0,0 +1,9 @@ +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'); + } +} diff --git a/framework/Filesystem/Driver/Driver.php b/framework/Filesystem/Driver/Driver.php new file mode 100644 index 0000000..9f4390f --- /dev/null +++ b/framework/Filesystem/Driver/Driver.php @@ -0,0 +1,44 @@ +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; + } +} diff --git a/framework/Filesystem/Driver/LocalDriver.php b/framework/Filesystem/Driver/LocalDriver.php new file mode 100644 index 0000000..33a2545 --- /dev/null +++ b/framework/Filesystem/Driver/LocalDriver.php @@ -0,0 +1,16 @@ +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'); + } +} diff --git a/framework/Http/Response.php b/framework/Http/Response.php new file mode 100644 index 0000000..1742f0b --- /dev/null +++ b/framework/Http/Response.php @@ -0,0 +1,104 @@ +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"); + } +} diff --git a/framework/Logging/Driver/Driver.php b/framework/Logging/Driver/Driver.php new file mode 100644 index 0000000..96ece0e --- /dev/null +++ b/framework/Logging/Driver/Driver.php @@ -0,0 +1,10 @@ +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; + } +} diff --git a/framework/Logging/Exception/DriverException.php b/framework/Logging/Exception/DriverException.php new file mode 100644 index 0000000..b24d526 --- /dev/null +++ b/framework/Logging/Exception/DriverException.php @@ -0,0 +1,9 @@ +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'); + } +} diff --git a/framework/Provider/CacheProvider.php b/framework/Provider/CacheProvider.php new file mode 100644 index 0000000..0a1a1be --- /dev/null +++ b/framework/Provider/CacheProvider.php @@ -0,0 +1,38 @@ + function($config) { + return new FileDriver($config); + }, + 'memcache' => function($config) { + return new MemcacheDriver($config); + }, + 'memory' => function($config) { + return new MemoryDriver($config); + }, + ]; + } +} diff --git a/framework/Provider/ConfigProvider.php b/framework/Provider/ConfigProvider.php new file mode 100644 index 0000000..1fc769b --- /dev/null +++ b/framework/Provider/ConfigProvider.php @@ -0,0 +1,16 @@ +bind('config', function($app) { + return new Config(); + }); + } +} diff --git a/framework/Provider/DatabaseProvider.php b/framework/Provider/DatabaseProvider.php new file mode 100644 index 0000000..4b10df7 --- /dev/null +++ b/framework/Provider/DatabaseProvider.php @@ -0,0 +1,34 @@ + function($config) { + return new SqliteConnection($config); + }, + 'mysql' => function($config) { + return new MysqlConnection($config); + }, + ]; + } +} diff --git a/framework/Provider/EmailProvider.php b/framework/Provider/EmailProvider.php new file mode 100644 index 0000000..e8448cf --- /dev/null +++ b/framework/Provider/EmailProvider.php @@ -0,0 +1,30 @@ + function($config) { + return new PostmarkDriver($config); + }, + ]; + } +} diff --git a/framework/Provider/FilesystemProvider.php b/framework/Provider/FilesystemProvider.php new file mode 100644 index 0000000..818306e --- /dev/null +++ b/framework/Provider/FilesystemProvider.php @@ -0,0 +1,30 @@ + function($config) { + return new LocalDriver($config); + }, + ]; + } +} diff --git a/framework/Provider/LoggingProvider.php b/framework/Provider/LoggingProvider.php new file mode 100644 index 0000000..e66aeff --- /dev/null +++ b/framework/Provider/LoggingProvider.php @@ -0,0 +1,30 @@ + function($config) { + return new StreamDriver($config); + }, + ]; + } +} diff --git a/framework/Provider/QueueProvider.php b/framework/Provider/QueueProvider.php new file mode 100644 index 0000000..08fbca1 --- /dev/null +++ b/framework/Provider/QueueProvider.php @@ -0,0 +1,30 @@ + function($config) { + return new DatabaseDriver($config); + }, + ]; + } +} diff --git a/framework/Provider/ResponseProvider.php b/framework/Provider/ResponseProvider.php new file mode 100644 index 0000000..130374d --- /dev/null +++ b/framework/Provider/ResponseProvider.php @@ -0,0 +1,16 @@ +bind('response', function($app) { + return new Response(); + }); + } +} diff --git a/framework/Provider/SessionProvider.php b/framework/Provider/SessionProvider.php new file mode 100644 index 0000000..8a2428b --- /dev/null +++ b/framework/Provider/SessionProvider.php @@ -0,0 +1,30 @@ + function($config) { + return new NativeDriver($config); + }, + ]; + } +} diff --git a/framework/Provider/ValidationProvider.php b/framework/Provider/ValidationProvider.php new file mode 100644 index 0000000..a543790 --- /dev/null +++ b/framework/Provider/ValidationProvider.php @@ -0,0 +1,34 @@ +bind('validator', function($app) { + $manager = new Manager(); + + $this->bindRules($app, $manager); + + return $manager; + }); + } + + private function bindRules(App $app, Manager $manager): void + { + $app->bind('validation.rule.required', fn() => new RequiredRule()); + $app->bind('validation.rule.email', fn() => new EmailRule()); + $app->bind('validation.rule.min', fn() => new MinRule()); + + $manager->addRule('required', $app->resolve('validation.rule.required')); + $manager->addRule('email', $app->resolve('validation.rule.email')); + $manager->addRule('min', $app->resolve('validation.rule.min')); + } +} diff --git a/framework/Provider/ViewProvider.php b/framework/Provider/ViewProvider.php new file mode 100644 index 0000000..231fb13 --- /dev/null +++ b/framework/Provider/ViewProvider.php @@ -0,0 +1,51 @@ +bind('view', function($app) { + $manager = new Manager(); + + $this->bindPaths($app, $manager); + $this->bindMacros($app, $manager); + $this->bindEngines($app, $manager); + + return $manager; + }); + } + + private function bindPaths(App $app, Manager $manager): void + { + $manager->addPath($app->resolve('paths.base') . '/resources/views'); + $manager->addPath($app->resolve('paths.base') . '/resources/images'); + } + + private function bindMacros(App $app, Manager $manager): void + { + $manager->addMacro('escape', fn($value) => htmlspecialchars($value, ENT_QUOTES)); + $manager->addMacro('includes', fn(...$params) => print view(...$params)); + } + + private function bindEngines(App $app, Manager $manager): void + { + $app->bind('view.engine.basic', fn() => new BasicEngine()); + $app->bind('view.engine.advanced', fn() => new AdvancedEngine()); + $app->bind('view.engine.php', fn() => new PhpEngine()); + $app->bind('view.engine.literal', fn() => new LiteralEngine()); + + $manager->addEngine('basic.php', $app->resolve('view.engine.basic')); + $manager->addEngine('advanced.php', $app->resolve('view.engine.advanced')); + $manager->addEngine('php', $app->resolve('view.engine.php')); + $manager->addEngine('svg', $app->resolve('view.engine.literal')); + } +} diff --git a/framework/Queue/Command/WorkCommand.php b/framework/Queue/Command/WorkCommand.php new file mode 100644 index 0000000..161b4cb --- /dev/null +++ b/framework/Queue/Command/WorkCommand.php @@ -0,0 +1,49 @@ +setDescription('Runs tasks that have been queued') + ->setHelp('This command waits for and runs queued jobs'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Waiting for jobs.'); + + while(true) { + if ($job = app('queue')->shift()) { + try { + $job->run(); + + $output->writeln("Completed {$job->id}"); + + $job->is_complete = true; + $job->save(); + + sleep(1); + } + catch (Exception $e) { + $message = $e->getMessage(); + $output->writeln("{$message}"); + + $job->attempts = $job->attempts + 1; + $job->save(); + } + } + } + } +} diff --git a/framework/Queue/Driver/DatabaseDriver.php b/framework/Queue/Driver/DatabaseDriver.php new file mode 100644 index 0000000..3de425e --- /dev/null +++ b/framework/Queue/Driver/DatabaseDriver.php @@ -0,0 +1,32 @@ +closure = serialize($wrapper); + $job->params = serialize($params); + $job->attempts = 0; + $job->save(); + + return $job->id; + } + + public function shift(): ?Job + { + $attempts = config('queue.database.attempts'); + + return Job::where('attempts', '<', $attempts) + ->where('is_complete', false) + ->first(); + } +} diff --git a/framework/Queue/Driver/Driver.php b/framework/Queue/Driver/Driver.php new file mode 100644 index 0000000..4a9ab2b --- /dev/null +++ b/framework/Queue/Driver/Driver.php @@ -0,0 +1,12 @@ +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'); + } +} diff --git a/framework/Queue/Job.php b/framework/Queue/Job.php new file mode 100644 index 0000000..d96d5bf --- /dev/null +++ b/framework/Queue/Job.php @@ -0,0 +1,21 @@ +closure); + $params = unserialize($this->params); + + return $closure(...$params); + } +} diff --git a/framework/Routing/Exception/RouteException.php b/framework/Routing/Exception/RouteException.php new file mode 100644 index 0000000..666fcce --- /dev/null +++ b/framework/Routing/Exception/RouteException.php @@ -0,0 +1,9 @@ +method = $method; + $this->path = $path; + $this->handler = $handler; + } + + public function method(): string + { + return $this->method; + } + + public function path(): string + { + return $this->path; + } + + public function parameters(): array + { + return $this->parameters; + } + + public function name(string $name = null) + { + if ($name) { + $this->name = $name; + return $this; + } + + return $this->name; + } + + public function matches(string $method, string $path): bool + { + if ( + $this->method === $method + && $this->path === $path + ) { + return true; + } + + $parameterNames = []; + + $pattern = $this->normalisePath($this->path); + + $pattern = preg_replace_callback('#{([^}]+)}/#', function (array $found) use (&$parameterNames) { + array_push($parameterNames, rtrim($found[1], '?')); + + if (str_ends_with($found[1], '?')) { + return '([^/]*)(?:/?)'; + } + + return '([^/]+)/'; + }, $pattern); + + if ( + !str_contains($pattern, '+') + && !str_contains($pattern, '*') + ) { + return false; + } + + preg_match_all("#{$pattern}#", $this->normalisePath($path), $matches); + + $parameterValues = []; + + if (count($matches[1]) > 0) { + foreach ($matches[1] as $value) { + if ($value) { + array_push($parameterValues, $value); + continue; + } + + array_push($parameterValues, null); + } + + $emptyValues = array_fill(0, count($parameterNames), false); + $parameterValues += $emptyValues; + + $this->parameters = array_combine($parameterNames, $parameterValues); + + return true; + } + + return false; + } + + private function normalisePath(string $path): string + { + $path = trim($path, '/'); + $path = "/{$path}/"; + $path = preg_replace('/[\/]{2,}/', '/', $path); + + return $path; + } + + public function dispatch() + { + if (is_array($this->handler)) { + [$class, $method] = $this->handler; + + if (is_string($class)) { + return app()->call([new $class, $method]); + } + + return app()->call([$class, $method]); + } + + return app()->call($this->handler); + } +} diff --git a/framework/Routing/Router.php b/framework/Routing/Router.php new file mode 100644 index 0000000..4d9597c --- /dev/null +++ b/framework/Routing/Router.php @@ -0,0 +1,131 @@ +routes[] = new Route($method, $path, $handler); + return $route; + } + + public function errorHandler(int $code, callable $handler) + { + $this->errorHandlers[$code] = $handler; + } + + public function dispatch() + { + $paths = $this->paths(); + + $requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET'; + $requestPath = $_SERVER['REQUEST_URI'] ?? '/'; + + $matching = $this->match($requestMethod, $requestPath); + + if ($matching) { + $this->current = $matching; + + try { + return $matching->dispatch(); + } + catch (Throwable $e) { + $result = null; + + if ($handler = config('handlers.exceptions')) { + $instance = new $handler(); + + if ($result = $instance->showThrowable($e)) { + return $result; + } + } + + return $this->dispatchError(); + } + } + + if (in_array($requestPath, $paths)) { + return $this->dispatchNotAllowed(); + } + + return $this->dispatchNotFound(); + } + + private function paths(): array + { + $paths = []; + + foreach ($this->routes as $route) { + $paths[] = $route->path(); + } + + return $paths; + } + + public function current(): ?Route + { + return $this->current; + } + + private function match(string $method, string $path): ?Route + { + foreach ($this->routes as $route) { + if ($route->matches($method, $path)) { + return $route; + } + } + + return null; + } + + public function dispatchNotAllowed() + { + $this->errorHandlers[400] ??= fn() => 'not allowed'; + return $this->errorHandlers[400](); + } + + public function dispatchNotFound() + { + $this->errorHandlers[404] ??= fn() => 'not found'; + return $this->errorHandlers[404](); + } + + public function dispatchError() + { + $this->errorHandlers[500] ??= fn() => 'server error'; + return $this->errorHandlers[500](); + } + + public function route(string $name, array $parameters = []): string + { + foreach ($this->routes as $route) { + if ($route->name() === $name) { + $finds = []; + $replaces = []; + + foreach ($parameters as $key => $value) { + array_push($finds, "{{$key}}"); + array_push($replaces, $value); + array_push($finds, "{{$key}?}"); + array_push($replaces, $value); + } + + $path = $route->path(); + $path = str_replace($finds, $replaces, $path); + $path = preg_replace('#{[^}]+}#', '', $path); + + return $path; + } + } + + throw new Exception('No route with that name'); + } +} diff --git a/framework/Session/Driver/Driver.php b/framework/Session/Driver/Driver.php new file mode 100644 index 0000000..3c50317 --- /dev/null +++ b/framework/Session/Driver/Driver.php @@ -0,0 +1,31 @@ +config = $config; + + if (session_status() !== PHP_SESSION_ACTIVE) { + session_start(); + } + } + + public function has(string $key): bool + { + $prefix = $this->config['prefix']; + return isset($_SESSION["{$prefix}{$key}"]); + } + + public function get(string $key, mixed $default = null): mixed + { + $prefix = $this->config['prefix']; + + if (isset($_SESSION["{$prefix}{$key}"])) { + return $_SESSION["{$prefix}{$key}"]; + } + + return $default; + } + + public function put(string $key, mixed $value): static + { + $prefix = $this->config['prefix']; + $_SESSION["{$prefix}{$key}"] = $value; + return $this; + } + + public function forget(string $key): static + { + $prefix = $this->config['prefix']; + unset($_SESSION["{$prefix}{$key}"]); + return $this; + } + + public function flush(): static + { + $prefix = $this->config['prefix']; + + foreach (array_keys($_SESSION) as $key) { + if (str_starts_with($key, $prefix)) { + unset($_SESSION[$key]); + } + } + + return $this; + } +} diff --git a/framework/Session/Exception/DriverException.php b/framework/Session/Exception/DriverException.php new file mode 100644 index 0000000..97c7fce --- /dev/null +++ b/framework/Session/Exception/DriverException.php @@ -0,0 +1,9 @@ +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'); + } +} diff --git a/framework/Support/Command/ServeCommand.php b/framework/Support/Command/ServeCommand.php new file mode 100644 index 0000000..e95d91d --- /dev/null +++ b/framework/Support/Command/ServeCommand.php @@ -0,0 +1,80 @@ +setDescription('Starts a development server') + ->setHelp('You can provide an optional host and port, for the development server.') + ->addOption('host', null, InputOption::VALUE_REQUIRED) + ->addOption('port', null, InputOption::VALUE_REQUIRED); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $base = app('paths.base'); + $host = $input->getOption('host') ?: env('APP_HOST', '127.0.0.1'); + $port = $input->getOption('port') ?: env('APP_PORT', '8000'); + + if (empty($host) || empty($port)) { + throw new InvalidArgumentException('APP_HOST and APP_PORT both need values'); + } + + $this->handleSignals(); + $this->startProcess($host, $port, $base, $output); + + return Command::SUCCESS; + } + + private function command(string $host, string $port, string $base): array + { + $separator = DIRECTORY_SEPARATOR; + + return [ + PHP_BINARY, + "-S", + "{$host}:{$port}", + "{$base}{$separator}server.php", + ]; + } + + private function handleSignals(): void + { + pcntl_async_signals(true); + + pcntl_signal(SIGTERM, function($signal) { + if ($signal === SIGTERM) { + $this->process->signal(SIGKILL); + exit; + } + }); + } + + private function startProcess(string $host, string $port, string $base, OutputInterface $output): void + { + $this->process = new Process($this->command($host, $port, $base), $base); + $this->process->setTimeout(PHP_INT_MAX); + + $this->process->start(function($type, $buffer) use ($output) { + $output->write("{$buffer}"); + }); + + $output->writeln("Serving requests at http://{$host}:{$port}"); + + $this->process->wait(); + } +} diff --git a/framework/Support/Config.php b/framework/Support/Config.php new file mode 100644 index 0000000..0690656 --- /dev/null +++ b/framework/Support/Config.php @@ -0,0 +1,44 @@ +loaded[$file])) { + $base = App::getInstance()->resolve('paths.base'); + $separator = DIRECTORY_SEPARATOR; + + $this->loaded[$file] = (array) require "{$base}{$separator}config{$separator}{$file}.php"; + } + + if ($value = $this->withDots($this->loaded[$file], $segments)) { + return $value; + } + + return $default; + } + + private function withDots(array $array, array $segments): mixed + { + $current = $array; + + foreach ($segments as $segment) { + if (!isset($current[$segment])) { + return null; + } + + $current = $current[$segment]; + } + + return $current; + } +} diff --git a/framework/Support/DriverFactory.php b/framework/Support/DriverFactory.php new file mode 100644 index 0000000..d7984af --- /dev/null +++ b/framework/Support/DriverFactory.php @@ -0,0 +1,11 @@ +name(); + $factory = $this->factory(); + $drivers = $this->drivers(); + + $app->bind($name, function ($app) use ($name, $factory, $drivers) { + foreach ($drivers as $key => $value) { + $factory->addDriver($key, $value); + } + + $config = config($name); + + return $factory->connect($config[$config['default']]); + }); + } + + abstract protected function name(): string; + abstract protected function factory(): DriverFactory; + abstract protected function drivers(): array; +} diff --git a/framework/Support/ExceptionHandler.php b/framework/Support/ExceptionHandler.php new file mode 100644 index 0000000..284b159 --- /dev/null +++ b/framework/Support/ExceptionHandler.php @@ -0,0 +1,40 @@ +showValidationException($throwable); + } + + if (isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'dev') { + $this->showFriendlyThrowable($throwable); + } + } + + public function showValidationException(ValidationException $exception) + { + if ($session = session()) { + $session->put($exception->getSessionName(), $exception->getErrors()); + } + + return redirect(env('HTTP_REFERER')); + } + + public function showFriendlyThrowable(Throwable $throwable) + { + $whoops = new Run(); + $whoops->pushHandler(new PrettyPageHandler()); + $whoops->register(); + + throw $throwable; + } +} diff --git a/framework/Testing/ServerExtension.php b/framework/Testing/ServerExtension.php new file mode 100644 index 0000000..6032b0f --- /dev/null +++ b/framework/Testing/ServerExtension.php @@ -0,0 +1,68 @@ +serverIsRunning()) { + $this->startedServer = false; + return; + } + + $this->startedServer = true; + + $base = app('paths.base'); + $separator = DIRECTORY_SEPARATOR; + + $this->process = new Process([ + PHP_BINARY, + "{$base}{$separator}command.php", + "serve" + ], $base); + + $this->process->start(function($type, $buffer) { + print $buffer; + }); + } + + private function serverIsRunning() + { + $connection = @fsockopen( + env('APP_HOST', '127.0.0.1'), + env('APP_PORT', '8000'), + ); + + if (is_resource($connection)) { + fclose($connection); + return true; + } + + return false; + } + + private function stopServer() + { + if ($this->startedServer) { + $this->process->signal(SIGTERM); + } + } + + public function executeBeforeFirstTest(): void + { + $this->startServer(); + } + + public function executeAfterLastTest(): void + { + $this->stopServer(); + } +} diff --git a/framework/Testing/TestCase.php b/framework/Testing/TestCase.php new file mode 100644 index 0000000..3f762f8 --- /dev/null +++ b/framework/Testing/TestCase.php @@ -0,0 +1,32 @@ +fail('exception was not thrown'); + } + catch (Throwable $e) { + $actualType = $e::class; + + if ($actualType !== $exceptionType) { + $this->fail("exception was {$actualType}, but expected {$exceptionType}"); + } + + $exception = $e; + } + + return [$exception, $result]; + } +} diff --git a/framework/Testing/TestResponse.php b/framework/Testing/TestResponse.php new file mode 100644 index 0000000..04f57eb --- /dev/null +++ b/framework/Testing/TestResponse.php @@ -0,0 +1,42 @@ +response = $response; + } + + public function isRedirecting(): bool + { + return $this->response->type() === Response::REDIRECT; + } + + public function redirectingTo(): ?string + { + return $this->response->redirect(); + } + + public function follow(): static + { + while ($this->isRedirecting()) { + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = $this->redirectingTo(); + $this->response = App::getInstance()->run(); + } + + return $this; + } + + public function __call(string $method, array $parameters = []): mixed + { + return $this->response->$method(...$parameters); + } +} diff --git a/framework/Validation/Exception/ValidationException.php b/framework/Validation/Exception/ValidationException.php new file mode 100644 index 0000000..911af82 --- /dev/null +++ b/framework/Validation/Exception/ValidationException.php @@ -0,0 +1,33 @@ +errors = $errors; + return $this; + } + + public function getErrors(): array + { + return $this->errors; + } + + public function setSessionName(string $sessionName): static + { + $this->sessionName = $sessionName; + return $this; + } + + public function getSessionName(): string + { + return $this->sessionName; + } +} diff --git a/framework/Validation/Manager.php b/framework/Validation/Manager.php new file mode 100644 index 0000000..d558dfc --- /dev/null +++ b/framework/Validation/Manager.php @@ -0,0 +1,57 @@ +rules[$alias] = $rule; + return $this; + } + + public function validate(array $data, array $rules, string $sessionName = 'errors'): array + { + $errors = []; + + foreach ($rules as $field => $rulesForField) { + foreach ($rulesForField as $rule) { + $name = $rule; + $params = []; + + if (str_contains($rule, ':')) { + [$name, $params] = explode(':', $rule); + $params = explode(',', $params); + } + + $processor = $this->rules[$name]; + + if (!$processor->validate($data, $field, $params)) { + if (!isset($errors[$field])) { + $errors[$field] = []; + } + + array_push($errors[$field], $processor->getMessage($data, $field, $params)); + } + } + } + + if (count($errors)) { + $exception = new ValidationException(); + $exception->setErrors($errors); + $exception->setSessionName($sessionName); + throw $exception; + } else { + if ($session = session()) { + $session->forget($sessionName); + } + } + + return array_intersect_key($data, $rules); + } +} diff --git a/framework/Validation/Rule/EmailRule.php b/framework/Validation/Rule/EmailRule.php new file mode 100644 index 0000000..2ef1ffb --- /dev/null +++ b/framework/Validation/Rule/EmailRule.php @@ -0,0 +1,20 @@ += $length; + } + + public function getMessage(array $data, string $field, array $params) + { + $length = (int) $params[0]; + + return "{$field} should be at least {$length} characters"; + } +} diff --git a/framework/Validation/Rule/RequiredRule.php b/framework/Validation/Rule/RequiredRule.php new file mode 100644 index 0000000..da564b6 --- /dev/null +++ b/framework/Validation/Rule/RequiredRule.php @@ -0,0 +1,16 @@ +path); + $folder = __DIR__ . '/../../../storage/framework/views'; + + if (!is_file("{$folder}/{$hash}.php")) { + touch("{$folder}/{$hash}.php"); + } + + $cached = realpath("{$folder}/{$hash}.php"); + + if (!file_exists($hash) || filemtime($view->path) > filemtime($hash)) { + $content = $this->compile(file_get_contents($view->path)); + file_put_contents($cached, $content); + } + + extract($view->data); + + ob_start(); + include($cached); + $contents = ob_get_contents(); + ob_end_clean(); + + if ($layout = $this->layouts[$cached] ?? null) { + $contentsWithLayout = view($layout, array_merge( + $view->data, + ['contents' => $contents], + )); + + return $contentsWithLayout; + } + + return $contents; + } + + protected function compile(string $template): string + { + // replace `@extends` with `$this->extends` + $template = preg_replace_callback('#@extends\(((?<=\().*(?=\)))\)#', function($matches) { + return 'extends(' . $matches[1] . '); ?>'; + }, $template); + + // replace `@if` with `if(...):` + $template = preg_replace_callback('#@if\(((?<=\().*(?=\)))\)#', function($matches) { + return ''; + }, $template); + + // replace `@endif` with `endif;` + $template = preg_replace_callback('#@endif#', function($matches) { + return ''; + }, $template); + + // replace `@foreach` with `foreach(...):` + $template = preg_replace_callback('#@foreach\(((?<=\().*(?=\)))\)#', function($matches) { + return ''; + }, $template); + + // replace `@endforeach` with `endforeach;` + $template = preg_replace_callback('#@endforeach#', function($matches) { + return ''; + }, $template); + + // replace `@[anything](...)` with `$this->[anything](...)` + $template = preg_replace_callback('#\s+@([^(]+)\(((?<=\().*(?=\)))\)#', function($matches) { + return '' . $matches[1] . '(' . $matches[2] . '); ?>'; + }, $template); + + // replace `{{ ... }}` with `print $this->escape(...)` + $template = preg_replace_callback('#\{\{([^}]*)\}\}#', function($matches) { + return 'escape(' . $matches[1] . '); ?>'; + }, $template); + + // replace `{!! ... !!}` with `print ...` + $template = preg_replace_callback('#\{!!([^}]+)!!\}#', function($matches) { + return ''; + }, $template); + + return $template; + } + + protected function extends(string $template): static + { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); + $this->layouts[realpath($backtrace[0]['file'])] = $template; + return $this; + } + + public function __call(string $name, $values) + { + return $this->manager->useMacro($name, ...$values); + } +} diff --git a/framework/View/Engine/BasicEngine.php b/framework/View/Engine/BasicEngine.php new file mode 100644 index 0000000..6f1a3b4 --- /dev/null +++ b/framework/View/Engine/BasicEngine.php @@ -0,0 +1,24 @@ +path); + + foreach ($view->data as $key => $value) { + $contents = str_replace( + '{'.$key.'}', $value, $contents + ); + } + + return $contents; + } +} diff --git a/framework/View/Engine/Engine.php b/framework/View/Engine/Engine.php new file mode 100644 index 0000000..bd64c6e --- /dev/null +++ b/framework/View/Engine/Engine.php @@ -0,0 +1,12 @@ +manager = $manager; + return $this; + } +} diff --git a/framework/View/Engine/LiteralEngine.php b/framework/View/Engine/LiteralEngine.php new file mode 100644 index 0000000..5e2f8eb --- /dev/null +++ b/framework/View/Engine/LiteralEngine.php @@ -0,0 +1,16 @@ +path); + } +} diff --git a/framework/View/Engine/PhpEngine.php b/framework/View/Engine/PhpEngine.php new file mode 100644 index 0000000..fdd619f --- /dev/null +++ b/framework/View/Engine/PhpEngine.php @@ -0,0 +1,47 @@ +data); + + ob_start(); + include($view->path); + $contents = ob_get_contents(); + ob_end_clean(); + + if ($layout = $this->layouts[$view->path] ?? null) { + $contentsWithLayout = view($layout, array_merge( + $view->data, + ['contents' => $contents], + )); + + return $contentsWithLayout; + } + + return $contents; + } + + protected function extends(string $template): static + { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); + $this->layouts[realpath($backtrace[0]['file'])] = $template; + return $this; + } + + public function __call(string $name, $values) + { + return $this->manager->useMacro($name, ...$values); + } +} diff --git a/framework/View/Manager.php b/framework/View/Manager.php new file mode 100644 index 0000000..c7286ff --- /dev/null +++ b/framework/View/Manager.php @@ -0,0 +1,59 @@ +paths, $path); + return $this; + } + + public function addEngine(string $extension, Engine $engine): static + { + $this->engines[$extension] = $engine; + $this->engines[$extension]->setManager($this); + return $this; + } + + public function render(string $template, array $data = []): View + { + foreach ($this->engines as $extension => $engine) { + foreach ($this->paths as $path) { + $file = "{$path}/{$template}.{$extension}"; + + if (is_file($file)) { + return new View($engine, realpath($file), $data); + } + } + } + + throw new Exception("Could not resolve '{$template}'"); + } + + public function addMacro(string $name, Closure $closure): static + { + $this->macros[$name] = $closure; + return $this; + } + + public function useMacro(string $name, ...$values) + { + if (isset($this->macros[$name])) { + $bound = $this->macros[$name]->bindTo($this); + return $bound(...$values); + } + + throw new Exception("Macro isn't defined: '{$name}'"); + } +} diff --git a/framework/View/View.php b/framework/View/View.php new file mode 100644 index 0000000..eda9d28 --- /dev/null +++ b/framework/View/View.php @@ -0,0 +1,19 @@ +engine->render($this); + } +} diff --git a/framework/helpers.php b/framework/helpers.php new file mode 100644 index 0000000..2e52d5d --- /dev/null +++ b/framework/helpers.php @@ -0,0 +1,132 @@ +resolve($alias); + } +} + +if (!function_exists('view')) { + function view(string $template, array $data = []): View\View + { + return app('view')->render($template, $data); + } +} + +if (!function_exists('validate')) { + function validate(array $data, array $rules, string $sessionName = 'errors') + { + return app('validator')->validate($data, $rules, $sessionName); + } +} + +if (!function_exists('response')) { + function response() + { + return app('response'); + } +} + +if (!function_exists('redirect')) { + function redirect(string $url) + { + return response()->redirect($url); + } +} + +if (!function_exists('csrf')) { + function csrf() + { + $session = session(); + + if (!$session) { + throw new Exception('Session is not enabled'); + } + + $session->put('token', $token = bin2hex(random_bytes(32))); + + return $token; + } +} + +if (!function_exists('secure')) { + function secure() + { + $session = session(); + + if (!$session) { + throw new Exception('Session is not enabled'); + } + + if (!isset($_POST['csrf']) || !$session->has('token') || !hash_equals($session->get('token'), $_POST['csrf'])) { + throw new Exception('CSRF token mismatch'); + } + } +} + +if (!function_exists('dd')) { + function dd(...$params) + { + var_dump(...$params); + die; + } +} + +if (!function_exists('basePath')) { + function basePath(string $newBasePath = null): ?string + { + return app('paths.base'); + } +} + +if (!function_exists('env')) { + function env(string $key, mixed $default = null): mixed + { + if (isset($_SERVER[$key])) { + return $_SERVER[$key]; + } + + if (isset($_ENV[$key])) { + return $_ENV[$key]; + } + + $value = getenv($key); + + if ($value !== false) { + return $value; + } + + return $default; + } +} + +if (!function_exists('config')) { + function config(string $key = null, mixed $default = null): mixed + { + if (is_null($key)) { + return app('config'); + } + + return app('config')->get($key, $default); + } +} + +if (!function_exists('session')) { + function session(string $key = null, mixed $default = null): mixed + { + if (is_null($key)) { + return app('session'); + } + + return app('session')->get($key, $default); + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e922b09 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,14 @@ + + + + tests + + + + + + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..f6e70b0 --- /dev/null +++ b/public/index.php @@ -0,0 +1,7 @@ +bind('paths.base', fn() => __DIR__ . '/..'); +$app->prepare()->run()->send(); diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..47364f1 --- /dev/null +++ b/readme.md @@ -0,0 +1,13 @@ +# Whoosh website + +To run the local development server, try: + +``` +export PHP_ENV=prod && php -S 127.0.0.1:8000 -t . +``` + +To make requests with cURL, try: + +``` +curl -X DELETE http://127.0.0.1:8000/old-home +``` diff --git a/resources/images/rocket.svg b/resources/images/rocket.svg new file mode 100644 index 0000000..1704518 --- /dev/null +++ b/resources/images/rocket.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + Outer space + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/views/home.advanced.php b/resources/views/home.advanced.php new file mode 100644 index 0000000..8132d5a --- /dev/null +++ b/resources/views/home.advanced.php @@ -0,0 +1,22 @@ +@extends('layout') +@includes('includes/large-feature') +@foreach($products as $i => $product) +
+
+

+ {{ $product->name }} +

+

+ {!! $product->description !!} +

+ + Order + +
+
+@endforeach diff --git a/resources/views/includes/large-feature.advanced.php b/resources/views/includes/large-feature.advanced.php new file mode 100644 index 0000000..8caa499 --- /dev/null +++ b/resources/views/includes/large-feature.advanced.php @@ -0,0 +1,24 @@ +
+
+
+ @includes('rocket') +
+
+ + Whoosh! + +
+ A place to buy rocket things +
+
    +
  1. Home
  2. + @if(isset($_SESSION['user_id'])) +
  3. Log out
  4. + @endif + @if(!isset($_SESSION['user_id'])) +
  5. Register
  6. + @endif +
+
+
+
diff --git a/resources/views/includes/small-feature.advanced.php b/resources/views/includes/small-feature.advanced.php new file mode 100644 index 0000000..4bbf635 --- /dev/null +++ b/resources/views/includes/small-feature.advanced.php @@ -0,0 +1,21 @@ +
+
+
+ @includes('rocket') +
+
+ + Whoosh! + +
    +
  1. Home
  2. + @if(isset($_SESSION['user_id'])) +
  3. Log out
  4. + @endif + @if(!isset($_SESSION['user_id'])) +
  5. Register
  6. + @endif +
+
+
+
diff --git a/resources/views/layout.advanced.php b/resources/views/layout.advanced.php new file mode 100644 index 0000000..14887c6 --- /dev/null +++ b/resources/views/layout.advanced.php @@ -0,0 +1,20 @@ + + + + Whoosh! + + + + + + + +
+ {!! $contents !!} +
+ + diff --git a/resources/views/products/view.advanced.php b/resources/views/products/view.advanced.php new file mode 100644 index 0000000..ebac09b --- /dev/null +++ b/resources/views/products/view.advanced.php @@ -0,0 +1,45 @@ +@extends('layout') +@includes('includes/large-feature') +
+

+ {{ $product->name }} +

+

+ {!! $product->description !!} +

+

+ Order +

+
+ @if(isset($_SESSION['errors'])) +
    + @foreach($_SESSION['errors'] as $field => $errors) + @foreach($errors as $error) +
  1. {{ $error }}
  2. + @endforeach + @endforeach +
+ @endif + + + +
+
diff --git a/resources/views/users/register.advanced.php b/resources/views/users/register.advanced.php new file mode 100644 index 0000000..e345c45 --- /dev/null +++ b/resources/views/users/register.advanced.php @@ -0,0 +1,106 @@ +@extends('layout') +@includes('includes/small-feature') +
+
+

+ Register +

+
+ @if(session()->has('register_errors')) +
    + @foreach(session()->get('register_errors') as $field => $errors) + @foreach($errors as $error) +
  1. {{ $error }}
  2. + @endforeach + @endforeach +
+ @endif + + + + + +
+
+
+

+ Log in +

+ +
+
diff --git a/server.php b/server.php new file mode 100644 index 0000000..de53b13 --- /dev/null +++ b/server.php @@ -0,0 +1,11 @@ +request('GET', 'http://127.0.0.1:8000/register'); + + $client->waitFor('.log-in-button'); + $client->executeScript("document.querySelector('.log-in-button').click()"); + + $client->waitFor('.log-in-errors'); + $element = $client->findElement(WebDriverBy::className('log-in-form')); + + $this->assertStringContainsString('email is required', $element->getText()); + $this->assertStringContainsString('password is required', $element->getText()); + } + + public function testRegisterForm() + { + $client = Client::createFirefoxClient(); + $client->request('GET', 'http://127.0.0.1:8000/register'); + + $client->waitFor('.register-button'); + $client->executeScript("document.querySelector('.register-button').click()"); + + $client->waitFor('.register-errors'); + $element = $client->findElement(WebDriverBy::className('register-form')); + + $this->assertStringContainsString('name is required', $element->getText()); + $this->assertStringContainsString('email is required', $element->getText()); + $this->assertStringContainsString('password is required', $element->getText()); + } +} diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php new file mode 100644 index 0000000..c9e417a --- /dev/null +++ b/tests/RoutingTest.php @@ -0,0 +1,32 @@ +assertStringContainsString($expected, app()->run()->content()); + } + + public function testRegistrationErrorsAreShown() + { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_SERVER['REQUEST_URI'] = '/register'; + $_SERVER['HTTP_REFERER'] = '/register'; + + $_POST['email'] = 'foo'; + $_POST['csrf'] = csrf(); + + $response = new TestResponse(app()->run()); + + $this->assertTrue($response->isRedirecting()); + $this->assertEquals($response->redirectingTo(), '/register'); + } +} diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php new file mode 100644 index 0000000..28e0f3f --- /dev/null +++ b/tests/ValidationTest.php @@ -0,0 +1,80 @@ +manager = new Manager(); + $this->manager->addRule('email', new EmailRule()); + $this->manager->addRule('min', new MinRule()); + $this->manager->addRule('required', new RequiredRule()); + } + + public function testInvalidEmailValuesFail() + { + $expected = ['email' => ['email should be an email']]; + + [ $exception ] = $this->assertExceptionThrown( + fn() => $this->manager->validate(['email' => 'foo'], ['email' => ['email']]), + ValidationException::class, + ); + + $this->assertEquals($expected, $exception->getErrors()); + } + + public function testValidEmailValuesPass() + { + $data = $this->manager->validate(['email' => 'foo@bar.com'], ['email' => ['email']]); + $this->assertEquals($data['email'], 'foo@bar.com'); + } + + public function testInvalidRequiredValuesFail() + { + $expected = ['email' => ['email is required']]; + + [ $exception ] = $this->assertExceptionThrown( + fn() => $this->manager->validate(['email' => ''], ['email' => ['required']]), + ValidationException::class, + ); + + $this->assertEquals($expected, $exception->getErrors()); + } + + public function testValidRequiredValuesPass() + { + $data = $this->manager->validate(['email' => 'foo@bar.com'], ['email' => ['required']]); + $this->assertEquals($data['email'], 'foo@bar.com'); + } + + public function testInvalidMinValuesFail() + { + $expected = ['email' => ['email should be at least 4 characters']]; + + [ $exception ] = $this->assertExceptionThrown( + fn() => $this->manager->validate(['email' => 'foo'], ['email' => ['min:4']]), + ValidationException::class, + ); + + $this->assertEquals($expected, $exception->getErrors()); + } + + public function testValidMinValuesPass() + { + $data = $this->manager->validate(['email' => 'foo@bar.com'], ['email' => ['min:4']]); + $this->assertEquals($data['email'], 'foo@bar.com'); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..b290fd8 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,7 @@ +bind('paths.base', fn() => __DIR__ . '/..'); +$app->prepare();