diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..22c2b04
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,11 @@
+# .gitattributes
+
+.github export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+tests export-ignore
+CHANGELOG.md export-ignore
+LICENSE.md export-ignore
+phpcs.xml export-ignore
+phpunit.xml export-ignore
+README.md export-ignore
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index a0d7e73..445ac00 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,16 @@
.idea
-/config/
+config
files
-Scheduler/Tests
storage
/vendor/
.env
+.env.development
+.env.local
+.env.production
+.env.staging
.phpcs-cache
.php-cs-fixer.cache
+.phpunit.cache
.phpunit.result.cache
composer.lock
index.php
\ No newline at end of file
diff --git a/Bootstrap/RegisterProviders.php b/Bootstrap/RegisterProviders.php
deleted file mode 100644
index 73475bf..0000000
--- a/Bootstrap/RegisterProviders.php
+++ /dev/null
@@ -1,80 +0,0 @@
-mergeAdditionalProviders($app);
- $app->registerConfiguredServiceProviders();
- }
-
- /**
- * Merge additional configured providers into the configuration.
- *
- * @param Application $app
- * @return void
- * @throws Exception
- */
- protected function mergeAdditionalProviders(Application $app): void
- {
- /** @var ConfigContainer $config */
- $config = $app->make(name: 'codefy.config');
-
- $arrayMerge = array_unique(
- array_merge(
- self::$merge,
- $config->getConfigKey(key: 'app.providers'),
- )
- );
-
- $config->setConfigKey(
- 'app',
- [
- 'providers' => $arrayMerge,
- ]
- );
- }
-
- /**
- * Merge the given providers into the provider configuration
- * before registration.
- *
- * @param array $providers
- * @return void
- */
- public static function merge(array $providers): void
- {
- self::$merge = array_values(
- array_filter(
- array_unique(
- array_merge(self::$merge, $providers)
- )
- )
- );
- }
-
- public static function flushState(): void
- {
- self::$merge = [];
- }
-}
diff --git a/Configuration/ApplicationBuilder.php b/Configuration/ApplicationBuilder.php
deleted file mode 100644
index 8cf41dc..0000000
--- a/Configuration/ApplicationBuilder.php
+++ /dev/null
@@ -1,98 +0,0 @@
-app->singleton(
- \Codefy\Framework\Contracts\Kernel::class,
- fn() => $this->app->make(name: \Codefy\Framework\Http\Kernel::class)
- );
-
- $this->app->singleton(
- key: \Codefy\Framework\Console\Kernel::class,
- value: fn() => $this->app->make(name: \Codefy\Framework\Console\ConsoleKernel::class)
- );
-
- return $this;
- }
-
- /**
- * Register additional service providers.
- *
- * @param array $providers
- * @return $this
- * @throws TypeException
- */
- public function withProviders(array $providers = []): self
- {
- RegisterProviders::merge($providers);
-
- foreach ($providers as $provider) {
- $this->app->registerServiceProvider($provider);
- }
-
- return $this;
- }
-
- /**
- * Register an array of singletons that are resource intensive
- * or are not called often.
- *
- * @param array $singletons
- * @return $this
- */
- public function withSingletons(array $singletons = []): self
- {
- if (empty($singletons)) {
- return $this;
- }
-
- foreach ($singletons as $key => $callable) {
- $this->app->singleton($key, $callable);
- };
-
- return $this;
- }
-
- /**
- * Register a callback to be invoked when the application's
- * service providers are registered.
- *
- * @param callable $callback
- * @return $this
- */
- public function registered(callable $callback): self
- {
- $this->app->registered(callback: $callback);
-
- return $this;
- }
-
- /**
- * Return the application instance.
- *
- * @return Application
- */
- public function return(): Application
- {
- return $this->app;
- }
-}
diff --git a/Contracts/Kernel.php b/Contracts/Kernel.php
deleted file mode 100644
index abd6bf2..0000000
--- a/Contracts/Kernel.php
+++ /dev/null
@@ -1,22 +0,0 @@
-getContainer();
- }
- return $app->getContainer()->make($name, $args);
-}
-
-/**
- * Get the available config instance.
- *
- * @param string $key
- * @param array|bool $set
- * @return mixed
- * @throws TypeException
- */
-function config(string $key, array|bool $set = false): mixed
-{
- if (!is_false__(var: $set)) {
- app(name: Collection::class)->setConfigKey($key, $set);
- return app(name: Collection::class)->getConfigKey($key);
- }
-
- return app(name: Collection::class)->getConfigKey($key);
-}
-
-/**
- * Retrieve a fresh instance of the bootstrap.
- *
- * @return mixed
- */
-function get_fresh_bootstrap(): mixed
-{
- if (file_exists(filename: $file = getcwd() . '/bootstrap/app.php')) {
- return require(realpath(path: $file));
- } elseif (file_exists(filename: $file = dirname(path: getcwd()) . '/bootstrap/app.php')) {
- return require(realpath(path: $file));
- } else {
- return require(realpath(path: dirname(path: getcwd()) . '/bootstrap/app.php'));
- }
-}
-
-/**
- * Gets the value of an environment variable.
- *
- * @param string $key
- * @param mixed|null $default
- * @return mixed|null
- */
-function env(string $key, mixed $default = null): mixed
-{
- return $_ENV[$key] ?? $default;
-}
-
-/**
- * OrmBuilder database instance.
- *
- * @return OrmBuilder|null
- * @throws Exception
- */
-function orm(): ?OrmBuilder
-{
- return Codefy::$PHP->getDB();
-}
-
-/**
- * Dbal database instance.
- *
- * @return Connection
- * @throws Exception
- */
-function dbal(): Connection
-{
- return Codefy::$PHP->getDbConnection();
-}
-
-/**
- * Alternative to PHP's native mail function with SMTP support.
- *
- * This is a simple mail function to see for testing or for
- * sending simple email messages.
- *
- * @param string|array $to Recipient(s)
- * @param string $subject Subject of the email.
- * @param string $message The email body.
- * @param array $headers An array of headers.
- * @param array $attachments An array of attachments.
- * @return bool
- * @throws Exception|ReflectionException|\PHPMailer\PHPMailer\Exception
- */
-function mail(string|array $to, string $subject, string $message, array $headers = [], array $attachments = []): bool
-{
- // Instantiate CodefyMailer.
- $instance = new CodefyMailer(config: app(name: 'codefy.config'));
-
- // Set the mailer transport.
- $func = sprintf('with%s', ucfirst(config(key: 'mailer.mail_transport')));
- $instance = $instance->{$func}();
-
- // Detect HTML markdown.
- if (substr_count(haystack: $message, needle: '') >= 1) {
- $instance = $instance->withHtml(isHtml: true);
- }
-
- // Build recipient(s).
- $instance = $instance->withTo(address: $to);
-
- // Set from name and from email from environment variables.
- $fromName = __observer()->filter->applyFilter('mail.from.name', env(key: 'MAILER_FROM_NAME'));
- $fromEmail = __observer()->filter->applyFilter('mail.from.email', env(key: 'MAILER_FROM_EMAIL'));
- // Set charset
- $charset = __observer()->filter->applyFilter('mail.charset', 'utf-8');
-
- // Set email subject and body.
- $instance = $instance->withSubject(subject: $subject)->withBody(data: $message);
-
- // Check for other headers and loop through them.
- if (!empty($headers)) {
- foreach ($headers as $name => $content) {
- if ($name === 'cc') {
- $instance = $instance->withCc(address: $content);
- }
-
- if ($name === 'bcc') {
- $instance = $instance->withBcc(address: $content);
- }
-
- if ($name === 'replyTo') {
- $instance = $instance->withReplyTo(address: $content);
- }
-
- if (
- ! in_array(needle: $name, haystack: ['MIME-Version','to','cc','bcc','replyTo'], strict: true)
- && !is_int($name)
- ) {
- $instance = $instance->withCustomHeader(name: (string) $name, value: $content);
- }
- }
- }
-
- // Set X-Mailer header
- $xMailer = __observer()->filter->applyFilter(
- 'mail.xmailer',
- sprintf('CodefyPHP Framework %s', Application::APP_VERSION)
- );
- $instance = $instance->withXMailer(xmailer: $xMailer);
-
- // Set email charset
- $instance = $instance->withCharset(charset: $charset ?: 'utf-8');
-
- // Check if there are attachments and loop through them.
- if (!empty($attachments)) {
- foreach ($attachments as $filename => $filepath) {
- $filename = is_string(value: $filename) ? $filename : '';
- $instance = $instance->withAttachment(path: $filepath, name: $filename);
- }
- }
-
- // Set sender.
- $instance = $instance->withFrom(address: $fromEmail, name: $fromName ?: '');
-
- try {
- return $instance->send();
- } catch (\PHPMailer\PHPMailer\Exception $e) {
- FileLoggerFactory::getLogger()->error($e->getMessage(), ['function' => '\Codefy\Framework\Helpers\mail']);
- return false;
- }
-}
-
-/**
- * Generate url's from named routes.
- *
- * @param string $name Name of the route.
- * @param array $params Data parameters.
- * @return string The url.
- * @throws NamedRouteNotFoundException
- * @throws RouteParamFailedConstraintException
- */
-function route(string $name, array $params = []): string
-{
- /** @var Router $route */
- $route = app('router');
- return $route->url($name, $params);
-}
diff --git a/Migration/Adapter/DbalMigrationAdapter.php b/Migration/Adapter/DbalMigrationAdapter.php
deleted file mode 100644
index 5b277a4..0000000
--- a/Migration/Adapter/DbalMigrationAdapter.php
+++ /dev/null
@@ -1,109 +0,0 @@
-connection;
- }
-
- /**
- * Get all migrated version numbers
- *
- * @return array
- * @throws Exception
- */
- public function fetchAll(): array
- {
- $tableName = $this->connection->quoteIdentifier($this->tableName);
- $sql = DB::query(query: "SELECT version FROM $tableName ORDER BY version ASC", type: DB::SELECT)->asAssoc();
- $all = $sql->execute($this->connection);
-
- return array_map(fn ($v) => $v['version'], $all);
- }
-
- /**
- * Up
- *
- * @param Migration $migration
- * @return MigrationAdapter
- */
- public function up(Migration $migration): MigrationAdapter
- {
- $this->connection->insert($this->tableName)
- ->values(
- [
- 'version' => $migration->getVersion(),
- 'recorded_on' => (new QubusDateTimeImmutable(time: 'now'))->format(format: 'Y-m-d h:i:s')
- ]
- )->execute();
-
- return $this;
- }
-
- /**
- * Down
- *
- * @param Migration $migration
- * @return MigrationAdapter
- */
- public function down(Migration $migration): MigrationAdapter
- {
- $this->connection->delete($this->tableName)
- ->where('version', $migration->getVersion())
- ->execute();
-
- return $this;
- }
-
- /**
- * Is the schema ready?
- *
- * @return bool
- * @throws Exception
- */
- public function hasSchema(): bool
- {
- $tables = $this->connection->listTables();
-
- if (in_array(needle: $this->tableName, haystack: $tables)) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Create Schema
- *
- * @return MigrationAdapter
- */
- public function createSchema(): MigrationAdapter
- {
- $this->connection->schema()->create($this->tableName, function (CreateTable $table) {
- $table->integer(name: 'id')->size(value: 'big')->autoincrement();
- $table->string(name: 'version', length: 191)->notNull();
- $table->dateTime(name: 'recorded_on');
- });
-
- return $this;
- }
-}
diff --git a/Migration/Adapter/FileMigrationAdapter.php b/Migration/Adapter/FileMigrationAdapter.php
deleted file mode 100644
index 2a7527c..0000000
--- a/Migration/Adapter/FileMigrationAdapter.php
+++ /dev/null
@@ -1,103 +0,0 @@
-filename = $filename;
- }
-
- /**
- * {@inheritdoc}
- */
- public function fetchAll(): array
- {
- $versions = file(filename: $this->filename, flags: FILE_IGNORE_NEW_LINES);
- sort($versions);
- return $versions;
- }
-
- /**
- * {@inheritdoc}
- * @throws TypeException
- */
- public function up(Migration $migration): MigrationAdapter
- {
- $versions = $this->fetchAll();
- if (in_array(needle: $migration->getVersion(), haystack: $versions)) {
- return $this;
- }
-
- $versions[] = $migration->getVersion();
- $this->write(versions: $versions);
- return $this;
- }
-
- /**
- * {@inheritdoc}
- * @throws TypeException
- */
- public function down(Migration $migration): MigrationAdapter
- {
- $versions = $this->fetchAll();
- if (!in_array(needle: $migration->getVersion(), haystack: $versions)) {
- return $this;
- }
-
- unset($versions[array_search(needle: $migration->getVersion(), haystack: $versions)]);
- $this->write(versions: $versions);
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function hasSchema(): bool
- {
- return file_exists(filename: $this->filename);
- }
-
- /**
- * {@inheritdoc}
- * @throws TypeException
- */
- public function createSchema(): MigrationAdapter
- {
- if (!is_writable(filename: dirname(path: $this->filename))) {
- throw new TypeException(message: sprintf('The file "%s" is not writeable', $this->filename));
- }
-
- if (false === touch(filename: $this->filename)) {
- throw new TypeException(message: sprintf('The file "%s" could not be written to', $this->filename));
- }
-
- return $this;
- }
-
- /**
- * Write to file
- *
- * @param array $versions
- * @throws TypeException
- */
- protected function write(array $versions): void
- {
- if (false === file_put_contents(filename: $this->filename, data: implode(separator: "\n", array: $versions))) {
- throw new TypeException(message: sprintf('The file "%s" could not be written to', $this->filename));
- }
- }
-}
diff --git a/Migration/Adapter/MigrationAdapter.php b/Migration/Adapter/MigrationAdapter.php
deleted file mode 100644
index 5a71743..0000000
--- a/Migration/Adapter/MigrationAdapter.php
+++ /dev/null
@@ -1,47 +0,0 @@
-version = $version;
- }
-
- /**
- * Init.
- *
- * @return void
- */
- public function init(): void
- {
- return;
- }
-
- /**
- * Do the migration.
- *
- * @return void
- */
- public function up(): void
- {
- return;
- }
-
- /**
- * Undo the migration.
- *
- * @return void
- */
- public function down(): void
- {
- return;
- }
-
- /**
- * Get adapter.
- *
- * @return MigrationAdapter
- */
- public function getAdapter(): MigrationAdapter
- {
- return $this->get('phpmig.adapter');
- }
-
- /**
- * Get Version.
- *
- * @return int|string|null
- */
- public function getVersion(): int|string|null
- {
- return $this->version;
- }
-
- /**
- * Set version.
- *
- * @param int|string $version
- * @return Migration
- */
- public function setVersion(int|string $version): static
- {
- $this->version = $version;
- return $this;
- }
-
- /**
- * Get name.
- *
- * @return string
- */
- public function getName(): string
- {
- return get_class(object: $this);
- }
-
- /**
- * Get ObjectMap.
- *
- * @return ArrayAccess
- */
- public function getObjectMap(): ArrayAccess
- {
- return $this->objectmap;
- }
-
- /**
- * Set ObjectMap.
- *
- * @param ArrayAccess $objectmap
- * @return Migration
- */
- public function setObjectMap(ArrayAccess $objectmap): static
- {
- $this->objectmap = $objectmap;
- return $this;
- }
-
-
- /**
- * Get Output.
- *
- * @return OutputInterface|null
- */
- public function getOutput(): ?OutputInterface
- {
- return $this->output;
- }
-
- /**
- * Set Output.
- *
- * @param OutputInterface $output
- * @return Migration
- */
- public function setOutput(OutputInterface $output): static
- {
- $this->output = $output;
- return $this;
- }
-
- /**
- * Get Input.
- *
- * @return InputInterface|null
- */
- public function getInput(): ?InputInterface
- {
- return $this->input;
- }
-
- /**
- * Set Input.
- *
- * @param InputInterface $input
- * @return Migration
- */
- public function setInput(InputInterface $input): static
- {
- $this->input = $input;
- return $this;
- }
-
- /**
- * Ask for input.
- *
- * @param Question $question
- * @return mixed
- */
- public function ask(Question $question): string
- {
- return $this->getDialogHelper()->ask(input: $this->getInput(), output: $this->getOutput(), question: $question);
- }
-
- /**
- * Get something from the objectmap
- *
- * @param string $key
- * @return mixed
- */
- public function get(string $key): mixed
- {
- $c = $this->getObjectMap();
- return $c[$key];
- }
-
- /**
- * Get Dialog Helper.
- *
- * @return QuestionHelper|null
- */
- public function getDialogHelper(): ?QuestionHelper
- {
- if ($this->dialogHelper) {
- return $this->dialogHelper;
- }
-
- return $this->dialogHelper = new QuestionHelper();
- }
-
- /**
- * Set Dialog Helper.
- *
- * @param QuestionHelper $dialogHelper
- * @return Migration
- */
- public function setDialogHelper(QuestionHelper $dialogHelper): static
- {
- $this->dialogHelper = $dialogHelper;
- return $this;
- }
-
- public function schema(): Schema
- {
- return $this->getAdapter()->connection()->schema();
- }
-}
diff --git a/Migration/Migrator.php b/Migration/Migrator.php
deleted file mode 100644
index ed582cd..0000000
--- a/Migration/Migrator.php
+++ /dev/null
@@ -1,159 +0,0 @@
-objectmap = $objectmap;
- $this->adapter = $adapter;
- $this->output = $output;
- }
-
- /**
- * Run the up method on a migration
- *
- * @param Migration $migration
- * @return void
- */
- public function up(Migration $migration): void
- {
- $this->run(migration: $migration, direction: 'up');
- }
-
- /**
- * Run the down method on a migration
- *
- * @param Migration $migration
- * @return void
- */
- public function down(Migration $migration): void
- {
- $this->run(migration: $migration, direction: 'down');
- }
-
- /**
- * Run a migration in a particular direction
- *
- * @param Migration $migration
- * @param string $direction
- * @return void
- */
- protected function run(Migration $migration, string $direction = 'up'): void
- {
- $direction = ($direction == 'down' ? 'down' : 'up');
- $this->getOutput()?->writeln(
- messages: sprintf(
- ' == ' .
- $migration->getVersion() . ' ' .
- $migration->getName() . ' ' .
- '' .
- ($direction == 'up' ? 'migrating' : 'reverting') .
- ''
- )
- );
- $start = microtime(as_float: true);
- $migration->setObjectMap($this->getObjectMap());
- $migration->init();
- $migration->{$direction}();
- $this->getAdapter()->{$direction}($migration);
- $end = microtime(as_float: true);
- $this->getOutput()?->writeln(
- messages: sprintf(
- ' == ' .
- $migration->getVersion() . ' ' .
- $migration->getName() . ' ' .
- '' .
- ($direction == 'up' ? 'migrated ' : 'reverted ') .
- sprintf("%.4fs", $end - $start) .
- ''
- )
- );
- }
-
- /**
- * Get ObjectMap.
- *
- * @return ArrayAccess
- */
- public function getObjectMap(): ArrayAccess
- {
- return $this->objectmap;
- }
-
- /**
- * Set ObjectMap.
- *
- * @param ArrayAccess $objectmap
- * @return Migrator
- */
- public function setObjectMap(ArrayAccess $objectmap): static
- {
- $this->objectmap = $objectmap;
- return $this;
- }
-
- /**
- * Get Adapter
- *
- * @return MigrationAdapter|null
- */
- public function getAdapter(): ?MigrationAdapter
- {
- return $this->adapter;
- }
-
- /**
- * Set Adapter
- *
- * @param MigrationAdapter $adapter
- * @return Migrator
- */
- public function setAdapter(MigrationAdapter $adapter): static
- {
- $this->adapter = $adapter;
- return $this;
- }
-
- /**
- * Get Output
- *
- * @return OutputInterface|null
- */
- public function getOutput(): ?OutputInterface
- {
- return $this->output;
- }
-
- /**
- * Set Output
- *
- * @param OutputInterface $output
- * @return Migrator
- */
- public function setOutput(OutputInterface $output): static
- {
- $this->output = $output;
- return $this;
- }
-}
diff --git a/Providers/PdoServiceProvider.php b/Providers/PdoServiceProvider.php
deleted file mode 100644
index 1ce5488..0000000
--- a/Providers/PdoServiceProvider.php
+++ /dev/null
@@ -1,50 +0,0 @@
-codefy->make('codefy.config');
-
- $default = $config->getConfigKey(key: 'database.default');
- $dsn = sprintf(
- '%s:dbname=%s;host=%s;charset=utf8mb4',
- $config->getConfigKey(key: "database.connections.{$default}.driver"),
- $config->getConfigKey(key: "database.connections.{$default}.dbname"),
- $config->getConfigKey(key: "database.connections.{$default}.host")
- );
-
- $this->codefy->define(name: PDO::class, args: [
- ':dsn' => $dsn,
- ':username' => $config->getConfigKey(
- key: "database.connections.{$default}.username"
- ),
- ':password' => $config->getConfigKey(
- key: "database.connections.{$default}.password"
- ),
- ':options' => [
- PDO::ATTR_EMULATE_PREPARES => false,
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
- PDO::ATTR_PERSISTENT => false,
- PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
- ],
- ]);
-
- $this->codefy->share(nameOrInstance: PDO::class);
- }
-}
diff --git a/README.md b/README.md
index feb4743..534674c 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-
+
@@ -16,8 +16,8 @@ CodefyPHP is __not__ a framework such as the likes of Symfony, Laravel, Codeigni
light framework providing interfaces and implementations for architecting a Domain Driven project with
CQRS, Event Sourcing and implementations of [PSR-3](https://www.php-fig.org/psr/psr-3),
[PSR-6](https://www.php-fig.org/psr/psr-6), [PSR-7](https://www.php-fig.org/psr/psr-7),
-[PSR-11](https://www.php-fig.org/psr/psr-11), [PSR-12](https://www.php-fig.org/psr/psr-12/),
-[PSR-15](https://www.php-fig.org/psr/psr-15), [PSR-16](https://www.php-fig.org/psr/psr-16)
+[PSR-11](https://www.php-fig.org/psr/psr-11), [PSR-12](https://www.php-fig.org/psr/psr-12/),
+[PSR-14](https://www.php-fig.org/psr/psr-14/), [PSR-15](https://www.php-fig.org/psr/psr-15), [PSR-16](https://www.php-fig.org/psr/psr-16)
and [PSR-17](https://www.php-fig.org/psr/psr-17).
The philosophy of Codefy is that code should be systematized, maintainable, and follow OOP (Object-Oriented Programming).
@@ -30,7 +30,7 @@ none of the features and instead use the interfaces to build your own implementa
## 📍 Requirement
-- PHP 8.2+
+- PHP 8.4+
- Additional constraints based on which components are used.
## 🏆 Highlighted Features
@@ -61,7 +61,7 @@ composer require codefyphp/codefy
|---------|---------------------|----------------|-----------------|----------------------|
| 1 | 8.2 | September 2023 | July 2024 | EOL |
| 2 - LTS | 8.2 | September 2024 | September 2027 | January 2028 |
-| 3.0 | 8.4 | December 2025 | August 2026 | December 2027 |
+| 3.0 | 8.4 | October 2025 | August 2026 | December 2027 |
| 3.1 | 8.4 | June 2025 | February 2027 | June 2028 |
## 📘 Documentation
diff --git a/Scheduler/BaseTask.php b/Scheduler/BaseTask.php
deleted file mode 100644
index 253ef6f..0000000
--- a/Scheduler/BaseTask.php
+++ /dev/null
@@ -1,283 +0,0 @@
-options = $options + [
- 'schedule' => null,
- 'maxRuntime' => 120,
- 'recipients' => null,
- 'smtpSender' => null,
- 'smtpSenderName' => null,
- 'enabled' => false,
- ];
-
- if ($this->options['schedule'] instanceof Expressional) {
- $this->expression = $this->options['schedule'];
- }
-
- if (is_string($this->options['schedule'])) {
- $this->expression = $this->alias($this->options['schedule']);
- }
-
- return $this;
- }
-
- public function getOption(?string $option): mixed
- {
- return $this->options[$option];
- }
-
- public function withTimezone(?string $timeZone): self
- {
- $this->timeZone = $timeZone;
-
- return $this;
- }
-
- /**
- * @throws TypeException
- */
- public function pid(): TaskId
- {
- $this->pid = TaskId::fromString();
-
- return $this->pid;
- }
-
- /**
- * Called before a task is executed.
- */
- public function setUp(): void
- {
- }
-
- /**
- * Executes a task.
- */
- abstract public function execute(Schedule $schedule): void;
-
- /**
- * Called after a task is executed.
- */
- public function tearDown(): void
- {
- }
-
- /**
- * Check if the Task is overlapping.
- * @throws TypeException
- */
- public function isOverlapping(): bool
- {
- return $this->acquireLock &&
- $this->cache->hasItem($this->pid()) &&
- call_user_func($this->whenOverlapping, $this->lockValue) === false;
- }
-
- /**
- * Check if time hasn't arrived.
- *
- * @param string $datetime
- * @return bool
- */
- protected function notYet(string $datetime): bool
- {
- return time() < strtotime($datetime);
- }
-
- /**
- * Check if the time has passed.
- *
- * @param string $datetime
- * @return bool
- */
- protected function past(string $datetime): bool
- {
- return time() > strtotime($datetime);
- }
-
- /**
- * Check if event should be run.
- *
- * @param string $datetime
- * @return self
- */
- public function from(string $datetime): self
- {
- return $this->skip(function () use ($datetime) {
- return $this->notYet($datetime);
- });
- }
-
- /**
- * Check if event should be not run.
- *
- * @param string $datetime
- * @return self
- */
- public function to(string $datetime): self
- {
- return $this->skip(function () use ($datetime) {
- return $this->past($datetime);
- });
- }
-
- /**
- * Acquires the Task lock.
- * @throws TypeException
- */
- protected function acquireTaskLock(): void
- {
- if ($this->acquireLock) {
- $item = $this->cache->getItem($this->pid());
-
- if (!$item->isHit()) {
- $item->set($this->options['maxRuntime'] + time());
- $item->expiresAfter($this->options['maxRuntime']);
- $this->cache->save($item);
- }
-
- $this->lockValue = $item->get();
- }
- }
-
- /**
- * Removes the Task lock.
- * @throws TypeException
- */
- protected function removeTaskLock(): bool
- {
- return $this->cache->deleteItem($this->pid());
- }
-
- /**
- * @throws TypeException
- * @throws Exception
- */
- public function checkMaxRuntime(): void
- {
- $maxRuntime = $this->options['maxRuntime'];
- if (is_null__($maxRuntime)) {
- return;
- }
-
- $runtime = $this->getLockLifetime($this->pid());
- if ($runtime < $maxRuntime) {
- return;
- }
-
- throw new Exception(
- sprintf(
- 'Max Runtime of %s secs exceeded! Current runtime for %s: %s secs.',
- $maxRuntime,
- static::class,
- $runtime
- ),
- 8712
- );
- }
-
- /**
- * Retrieve the Task lock lifetime value.
- */
- private function getLockLifetime(TaskId $lockKey): int
- {
- if (!$this->cache->hasItem($lockKey)) {
- return 0;
- }
-
- return time() - $this->lockValue;
- }
-
- /**
- * Checks whether the task can and should run.
- * @throws TypeException
- */
- private function shouldRun(): bool
- {
- if (is_false__($this->options['enabled'])) {
- return false;
- }
-
- // If task overlaps don't execute.
- if ($this->isOverlapping()) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Checks whether the task can and should run.
- * @throws TypeException
- * @throws NativeException
- */
- public function isDue(string|DateTimeZone|null $timeZone = null): bool
- {
- if (is_false__($this->shouldRun())) {
- return false;
- }
-
- if (is_null__($timeZone)) {
- $timeZone = date_default_timezone_get();
- }
-
- $isDue = $this->expression->isDue(currentTime: 'now', timeZone: $this->timeZone ?? $timeZone);
-
- if (parent::isDue($this->timeZone ?? $timeZone) && $isDue) {
- return true;
- }
-
- return false;
- }
-}
diff --git a/Scheduler/Event/TaskCompleted.php b/Scheduler/Event/TaskCompleted.php
deleted file mode 100644
index 2988675..0000000
--- a/Scheduler/Event/TaskCompleted.php
+++ /dev/null
@@ -1,25 +0,0 @@
-task = $task;
- }
-
- public function getTask(): Task
- {
- return $this->task;
- }
-}
diff --git a/Scheduler/Event/TaskFailed.php b/Scheduler/Event/TaskFailed.php
deleted file mode 100644
index ceaf433..0000000
--- a/Scheduler/Event/TaskFailed.php
+++ /dev/null
@@ -1,25 +0,0 @@
-task = $task;
- }
-
- public function getTask(): Task
- {
- return $this->task;
- }
-}
diff --git a/Scheduler/Event/TaskSkipped.php b/Scheduler/Event/TaskSkipped.php
deleted file mode 100644
index ca60923..0000000
--- a/Scheduler/Event/TaskSkipped.php
+++ /dev/null
@@ -1,12 +0,0 @@
-task = $task;
- }
-
- public function getTask(): Task
- {
- return $this->task;
- }
-}
diff --git a/Scheduler/FailedProcessor.php b/Scheduler/FailedProcessor.php
deleted file mode 100644
index 7ae4734..0000000
--- a/Scheduler/FailedProcessor.php
+++ /dev/null
@@ -1,29 +0,0 @@
-processor;
- }
-
- public function getException(): Exception
- {
- return $this->exception;
- }
-}
diff --git a/Scheduler/Stack.php b/Scheduler/Stack.php
deleted file mode 100644
index f7c47e2..0000000
--- a/Scheduler/Stack.php
+++ /dev/null
@@ -1,133 +0,0 @@
-dispatcher = $dispatcher;
- $this->mutex = $mutex;
- $this->timezone = $timezone;
- }
-
- /**
- * Add tasks to the list.
- *
- * @param Task[] $tasks
- */
- public function addTasks(array $tasks): void
- {
- foreach ($tasks as $task) {
- $this->addTask($task);
- }
- }
-
- /**
- * Add a task to the list.
- */
- public function addTask(Task $task): Stack
- {
- $this->tasks[] = $task;
-
- return $this;
- }
-
- /**
- * Returns an array of tasks.
- */
- public function tasks(): array
- {
- return $this->tasks;
- }
-
- /**
- * Gets tasks that are due and can run.
- *
- * @return array
- */
- public function tasksDue(): array
- {
- $that = clone $this;
-
- return array_filter($this->tasks, function (Task $task) use ($that) {
- return $task->isDue($that->timezone);
- });
- }
-
- public function withOptions(array $options = []): self
- {
- $resolver = new OptionsResolver();
- $this->configureOptions($resolver);
-
- $this->options = $resolver->resolve($options);
-
- return $this;
- }
-
- protected function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'dispatcher' => $this->dispatcher,
- 'mutex' => $this->mutex,
- 'timezone' => $this->timezone,
- 'maxRuntime' => 120,
- 'encryption' => null,
- ]);
- }
-
- public function run(): void
- {
- $schedule = new Schedule($this->options['timezone'], $this->options['mutex']);
-
- foreach ($this->tasks() as $task) {
- if (is_false__($task->getOption('enabled'))) {
- continue;
- }
-
- $this->dispatcher->dispatch(TaskStarted::EVENT_NAME);
-
- try {
- // Run code before executing task.
- $task->setUp();
- // Execute task.
- $task->execute($schedule);
- // Run code after executing task.
- $task->tearDown();
- } catch (Exception $ex) {
- $this->dispatcher->dispatch(TaskFailed::EVENT_NAME);
- $this->sendEmail($ex);
- }
-
- $this->dispatcher->dispatch(TaskCompleted::EVENT_NAME);
- }
- }
-}
diff --git a/Scheduler/Task.php b/Scheduler/Task.php
deleted file mode 100644
index ddec023..0000000
--- a/Scheduler/Task.php
+++ /dev/null
@@ -1,44 +0,0 @@
-=8.2",
+ "php": ">=8.4",
"ext-pdo": "*",
- "codefyphp/domain-driven-core": "^1",
+ "codefyphp/domain-driven-core": "^3",
+ "composer/class-map-generator": "^1",
"dragonmantank/cron-expression": "^3",
- "qubus/cache": "^3",
- "qubus/error": "^2",
- "qubus/event-dispatcher": "^3",
- "qubus/exception": "^3",
- "qubus/expressive": "^1",
- "qubus/filesystem": "^3",
- "qubus/inheritance": "^3",
- "qubus/injector": "^3",
- "qubus/mail": "^4",
- "qubus/router": "^3",
- "qubus/security": "^3",
- "qubus/support": "^3",
- "qubus/view": "^2",
- "symfony/console": "^6",
- "symfony/options-resolver": "^6"
+ "melbahja/seo": "^2",
+ "middlewares/cache": "^3",
+ "middlewares/minifier": "^2",
+ "middlewares/referrer-spam": "^2",
+ "middlewares/whoops": "^2",
+ "paragonie/csp-builder": "^3",
+ "php-debugbar/php-debugbar": "^2.2",
+ "qubus/cache": "^4",
+ "qubus/error": "^3",
+ "qubus/event-dispatcher": "^4",
+ "qubus/exception": "^4",
+ "qubus/expressive": "^3",
+ "qubus/filesystem": "^4",
+ "qubus/inheritance": "^4",
+ "qubus/mail": "^5",
+ "qubus/nosql": "^4",
+ "qubus/router": "^4",
+ "qubus/security": "^4",
+ "qubus/support": "^4",
+ "qubus/view": "^3",
+ "symfony/console": "^7",
+ "symfony/options-resolver": "^7"
},
"autoload": {
"psr-4": {
- "Codefy\\Framework\\": ""
+ "Codefy\\Framework\\": "src/"
},
"files": [
- "Helpers/core.php",
- "Helpers/path.php"
+ "src/Helpers/core.php",
+ "src/Helpers/path.php",
+ "src/Http/Middleware/Csrf/helpers.php"
]
},
"require-dev": {
"fenom/fenom": "^3.0",
"fenom/providers-collection": "^1.0",
"foil/foil": "^0.6.7",
- "mockery/mockery": "^1.3.1",
- "pestphp/pest": "^1.22",
- "pestphp/pest-plugin-mock": "^1.0",
- "qubus/qubus-coding-standard": "^1.1"
+ "qubus/qubus-coding-standard": "^2"
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Codefy\\Framework\\Tests\\": "tests/"
+ }
},
"scripts": {
- "test": "XDEBUG_MODE=coverage vendor/bin/pest --coverage --min=50 --colors=always",
+ "test": "vendor/bin/pest",
"cs-check": "phpcs",
"cs-fix": "phpcbf"
},
diff --git a/phpcs.xml b/phpcs.xml
index 1b99f57..9a0285f 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -12,20 +12,23 @@
- Auth
- Bootstrap
- Console
- Contracts
- Factory
- Helpers
- Http
- Migration
- Providers
- Scheduler
- Support
- View
- Application.php
- Codefy.php
+ src/Auth
+ src/Bootstrap
+ src/Configuration
+ src/Console
+ src/Contracts
+ src/Factory
+ src/Helpers
+ src/Http
+ src/Migration
+ src/Pipeline
+ src/Providers
+ src/Proxy
+ src/Scheduler
+ src/Support
+ src/Traits
+ src/View
+ src/Application.php
diff --git a/phpunit.xml b/phpunit.xml
index 2ec8627..b1bedaf 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,21 +1,17 @@
-
-
-
- ./tests
-
-
-
-
- ./vendor
- ./tests
-
-
- ./
-
-
+
+
+
+ ./tests
+
+
+
+
+ ./src
+
+
+ ./vendor
+ ./tests
+
+
diff --git a/Application.php b/src/Application.php
similarity index 69%
rename from Application.php
rename to src/Application.php
index 03eae67..a278a40 100644
--- a/Application.php
+++ b/src/Application.php
@@ -4,27 +4,34 @@
namespace Codefy\Framework;
+use Codefy\Framework\Bootstrap\RegisterProviders;
use Codefy\Framework\Configuration\ApplicationBuilder;
+use Codefy\Framework\Contracts\Http\Kernel;
use Codefy\Framework\Factory\FileLoggerFactory;
-use Codefy\Framework\Factory\FileLoggerSmtpFactory;
use Codefy\Framework\Pipeline\PipelineBuilder;
+use Codefy\Framework\Proxy\Codefy;
+use Codefy\Framework\Support\Assets;
use Codefy\Framework\Support\BasePathDetector;
use Codefy\Framework\Support\LocalStorage;
use Codefy\Framework\Support\Paths;
+use Codefy\Framework\Traits\LoggerAware;
+use Defuse\Crypto\Exception\BadFormatException;
+use Defuse\Crypto\Exception\EnvironmentIsBrokenException;
+use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException;
use Dotenv\Dotenv;
use Psr\Container\ContainerInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
-use Psr\Log\LoggerInterface;
use Qubus\Config\ConfigContainer;
-use Qubus\Dbal\Connection;
-use Qubus\Dbal\DB;
use Qubus\EventDispatcher\ActionFilter\Observer;
-use Qubus\EventDispatcher\EventDispatcher;
use Qubus\Exception\Data\TypeException;
use Qubus\Exception\Exception;
-use Qubus\Expressive\OrmBuilder;
+use Qubus\Expressive\Connection;
+use Qubus\Expressive\Connection\DriverConnection;
+use Qubus\Expressive\QueryBuilder;
use Qubus\Http\Cookies\Factory\HttpCookieFactory;
+use Qubus\Http\Encryption\Env\SecureEnv;
use Qubus\Http\Session\Flash;
use Qubus\Http\Session\PhpSession;
use Qubus\Inheritance\InvokerAware;
@@ -35,45 +42,33 @@
use Qubus\Injector\ServiceProvider\Bootable;
use Qubus\Injector\ServiceProvider\Serviceable;
use Qubus\Mail\Mailer;
+use Qubus\Routing\Router;
use Qubus\Support\ArrayHelper;
-use Qubus\Support\Assets;
use Qubus\Support\StringHelper;
use ReflectionException;
+use function array_merge;
+use function array_unique;
use function dirname;
use function get_class;
use function is_string;
use function Qubus\Config\Helpers\env;
use function Qubus\Support\Helpers\is_null__;
use function rtrim;
+use function strtoupper;
use const DIRECTORY_SEPARATOR;
-/**
- * @property-read ServerRequestInterface $request
- * @property-read ResponseInterface $response
- * @property-read Assets $assets
- * @property-read Mailer $mailer
- * @property-read PhpSession $session
- * @property-read Flash $flash
- * @property-read EventDispatcher $event
- * @property-read HttpCookieFactory $httpCookie
- * @property-read LocalStorage $localStorage
- * @property-read ConfigContainer $configContainer
- * @property-read PipelineBuilder $pipeline
- * @property-read Observer $hook
- * @property-read StringHelper $string
- * @property-read ArrayHelper $array
- */
final class Application extends Container
{
use InvokerAware;
+ use LoggerAware;
- public const APP_VERSION = '2.1.5';
+ public const string APP_VERSION = '3.0.0';
- public const MIN_PHP_VERSION = '8.2';
+ public const string MIN_PHP_VERSION = '8.4';
- public const DS = DIRECTORY_SEPARATOR;
+ public const string DS = DIRECTORY_SEPARATOR;
/**
* The current globally available Application (if any).
@@ -82,40 +77,89 @@ final class Application extends Container
*/
public static ?Application $APP = null;
- public string $charset = 'UTF-8';
+ // phpcs:disable
+ /**
+ * @var string Charset of the application.
+ */
+ public string $charset = 'UTF-8' {
+ get => $this->charset;
+ set(string $charset) => $this->charset = strtoupper($charset);
+ }
+
+ /**
+ * @var string Language of the application.
+ */
+ public string $language = 'en' {
+ get => $this->language;
+ set(string $language) => $this->language = strtoupper($language);
+ }
- public string $locale = 'en';
+ /**
+ * @var string Language of translatable source files.
+ */
+ public string $locale = 'en' {
+ get => $this->locale;
+ set(string $locale) => $this->locale = $locale;
+ }
- public string $controllerNamespace = 'App\\Infrastructure\\Http\\Controllers';
+ public string $controllerNamespace = 'App\\Infrastructure\\Http\\Controllers' {
+ get => $this->controllerNamespace;
+ set(string $controllerNamespace) => $this->controllerNamespace = $controllerNamespace;
+ }
public static string $ROOT_PATH = '';
- protected string $basePath = '';
+ public static bool $encryptedEnv = false;
+
+ private string $basePath = '' {
+ get => $this->basePath;
+ }
- protected ?string $appPath = null;
+ private ?string $appPath = null {
+ get => $this->appPath;
+ }
- protected array $serviceProviders = [];
+ private array $serviceProviders = [] {
+ &get => $this->serviceProviders;
+ }
- protected array $serviceProvidersRegistered = [];
+ private array $serviceProvidersRegistered = [] {
+ &get => $this->serviceProvidersRegistered;
+ }
- protected array $baseMiddlewares = [];
+ private array $baseMiddlewares = [] {
+ &get => $this->baseMiddlewares;
+ }
- private bool $booted = false;
+ public private(set) bool $booted = false;
- private bool $hasBeenBootstrapped = false;
+ private bool $hasBeenBootstrapped = false {
+ get => $this->hasBeenBootstrapped;
+ }
- protected array $bootingCallbacks = [];
+ private array $bootingCallbacks = [] {
+ &get => $this->bootingCallbacks;
+ }
- protected array $bootedCallbacks = [];
+ private array $bootedCallbacks = [] {
+ &get => $this->bootedCallbacks;
+ }
- protected array $registeredCallbacks = [];
+ private array $registeredCallbacks = [] {
+ &get => $this->registeredCallbacks;
+ }
- private array $param = [];
+ private array $param = [] {
+ &get => $this->param;
+ }
+ // phpcs:enable
/**
+ * @param array $params
+ * @throws ReflectionException
* @throws TypeException
*/
- public function __construct(array $params)
+ public function __construct(array $params = [])
{
if (isset($params['basePath'])) {
$this->withBasePath(basePath: $params['basePath']);
@@ -124,7 +168,8 @@ public function __construct(array $params)
parent::__construct(InjectorFactory::create(config: $this->coreAliases()));
$this->registerBaseBindings();
$this->registerDefaultServiceProviders();
- $this->registerPropertyBindings();
+
+ Codefy::$PHP = $this::getInstance();
}
private function registerBaseBindings(): void
@@ -134,38 +179,6 @@ private function registerBaseBindings(): void
$this->alias(original: 'app', alias: self::class);
}
- /**
- * Dynamically created properties.
- *
- * @return void
- * @throws TypeException
- */
- private function registerPropertyBindings(): void
- {
- $contracts = [
- 'request' => ServerRequestInterface::class,
- 'response' => ResponseInterface::class,
- 'assets' => Assets::class,
- 'mailer' => Mailer::class,
- 'session' => PhpSession::class,
- 'flash' => Flash::class,
- 'event' => EventDispatcher::class,
- 'httpCookie' => HttpCookieFactory::class,
- 'localStorage' => Support\LocalStorage::class,
- 'configContainer' => ConfigContainer::class,
- 'pipeline' => PipelineBuilder::class,
- 'hook' => Observer::class,
- 'string' => StringHelper::class,
- 'array' => ArrayHelper::class,
- ];
-
- foreach ($contracts as $property => $name) {
- $this->{$property} = $this->make(name: $name);
- }
-
- Codefy::$PHP = $this::getInstance();
- }
-
/**
* Infer the application's base directory
* from the environment and server.
@@ -174,36 +187,15 @@ private function registerPropertyBindings(): void
*/
protected static function inferBasePath(): ?string
{
- $basePath = (new BasePathDetector())->getBasePath();
+ $basePath = new BasePathDetector()->getBasePath();
return match (true) {
- (env('APP_BASE_PATH') !== null && env('APP_BASE_PATH') !== false) => env('APP_BASE_PATH'),
+ (env(key: 'APP_BASE_PATH') !== null && env(key: 'APP_BASE_PATH') !== false) => env(key: 'APP_BASE_PATH'),
$basePath !== '' => $basePath,
default => dirname(path: __FILE__, levels: 2),
};
}
- /**
- * FileLogger
- *
- * @throws ReflectionException
- */
- public static function getLogger(): LoggerInterface
- {
- return FileLoggerFactory::getLogger();
- }
-
- /**
- * FileLogger with SMTP support.
- *
- * @throws ReflectionException
- * @throws TypeException
- */
- public static function getSmtpLogger(): LoggerInterface
- {
- return FileLoggerSmtpFactory::getLogger();
- }
-
/**
* @throws Exception
*/
@@ -211,31 +203,18 @@ public function getDbConnection(): Connection
{
/** @var ConfigContainer $config */
$config = $this->make(name: 'codefy.config');
+ $default = $config->getConfigKey(key: 'database.default');
- $connection = $config->getConfigKey(key: 'database.default');
-
- return DB::connection([
- 'driver' => $config->getConfigKey(key: "database.connections.{$connection}.driver"),
- 'host' => $config->getConfigKey(key: "database.connections.{$connection}.host", default: 'localhost'),
- 'port' => $config->getConfigKey(key: "database.connections.{$connection}.port", default: 3306),
- 'charset' => $config->getConfigKey(key: "database.connections.{$connection}.charset", default: 'utf8mb4'),
- 'collation' => $config->getConfigKey(
- key: "database.connections.{$connection}.collation",
- default: 'utf8mb4_unicode_ci'
- ),
- 'username' => $config->getConfigKey(key: "database.connections.{$connection}.username"),
- 'password' => $config->getConfigKey(key: "database.connections.{$connection}.password"),
- 'dbname' => $config->getConfigKey(key: "database.connections.{$connection}.dbname"),
- ]);
+ return DriverConnection::make($config->getConfigKey(key: "database.connections.{$default}"));
}
/**
- * @return OrmBuilder|null
+ * @return QueryBuilder|null
* @throws Exception
*/
- public function getDB(): ?OrmBuilder
+ public function getDb(): ?QueryBuilder
{
- return new OrmBuilder(connection: $this->getDbConnection());
+ return QueryBuilder::fromInstance($this->getDbConnection());
}
/**
@@ -278,8 +257,10 @@ protected function registerDefaultServiceProviders(): void
foreach (
[
Providers\ConfigServiceProvider::class,
- Providers\PdoServiceProvider::class,
+ Providers\EventDispatcherServiceProvider::class,
+ Providers\DatabaseConnectionServiceProvider::class,
Providers\FlysystemServiceProvider::class,
+ Providers\RouterServiceProvider::class,
] as $serviceProvider
) {
$this->registerServiceProvider(serviceProvider: $serviceProvider);
@@ -292,11 +273,11 @@ public function bootstrapWith(array $bootstrappers): void
$this->hasBeenBootstrapped = true;
foreach ($bootstrappers as $bootstrapper) {
- $this->make(name: EventDispatcher::class)->dispatch("bootstrapping.{$bootstrapper}");
+ $this->make(name: EventDispatcherInterface::class)->dispatch(new $bootstrapper());
$this->make(name: $bootstrapper)->bootstrap($this);
- $this->make(name: EventDispatcher::class)->dispatch("bootstrapped.{$bootstrapper}");
+ $this->make(name: EventDispatcherInterface::class)->dispatch(new $bootstrapper());
}
}
@@ -305,16 +286,11 @@ public function getContainer(): ServiceContainer|ContainerInterface
return $this;
}
- public function setBoot(bool $bool = false): void
+ public function setBooted(bool $bool = false): void
{
$this->booted = $bool;
}
- public function isBooted(): bool
- {
- return $this->booted;
- }
-
/**
* @return void
* @throws TypeException
@@ -363,7 +339,8 @@ public function registerConfiguredServiceProviders(): void
/** @var ConfigContainer $config */
$config = $this->make(name: 'codefy.config');
- $providers = $config->getConfigKey(key: 'app.providers');
+ $appProviders = $config->getConfigKey(key: 'app.providers');
+ $providers = array_unique(array_merge($appProviders, RegisterProviders::$providers));
foreach ($providers as $serviceProvider) {
$this->registerServiceProvider(serviceProvider: $serviceProvider);
@@ -395,10 +372,10 @@ public function registerServiceProvider(
$serviceProvider->register();
- $this->markServiceProviderAsRegistered(registered: $registered, serviceProvider: $serviceProvider);
+ $this->markServiceProviderAsRegistered(serviceProvider: $serviceProvider);
// If application is booted, call the boot method on the service provider
// if it exists.
- if ($this->isBooted()) {
+ if ($this->booted) {
$this->bootServiceProvider(provider: $serviceProvider);
};
@@ -408,12 +385,14 @@ public function registerServiceProvider(
/**
* Get the registered service provider instance if it exists.
*
- * @param Serviceable|Bootable|string $provider
- * @return string|null
+ * @param Serviceable|Bootable|string $provider
+ * @return Serviceable|Bootable|null
*/
- public function getRegisteredServiceProvider(Serviceable|Bootable|string $provider): string|null
+ public function getRegisteredServiceProvider(Serviceable|Bootable|string $provider): Serviceable|Bootable|null
{
- return $this->serviceProviders[$provider] ?? null;
+ $registered = is_string(value: $provider) ? $provider : get_class(object: $provider);
+
+ return $this->serviceProviders[$registered] ?? null;
}
/**
@@ -443,7 +422,7 @@ public function resolveServiceProvider(string $provider): BaseServiceProvider
/**
* Get the service providers that have been registered.
*
- * @return array
+ * @return array
*/
public function getRegisteredProviders(): array
{
@@ -453,12 +432,14 @@ public function getRegisteredProviders(): array
/**
* Determine if the given service provider is registered.
*
- * @param string $provider
+ * @param string|Serviceable|Bootable $provider
* @return bool
*/
- public function providerIsRegistered(string $provider): bool
+ public function providerIsRegistered(string|Serviceable|Bootable $provider): bool
{
- return isset($this->serviceProvidersRegistered[$provider]);
+ $registered = is_string(value: $provider) ? $provider : get_class(object: $provider);
+
+ return isset($this->serviceProvidersRegistered[$registered]);
}
/**
@@ -470,9 +451,13 @@ public function providerIsRegistered(string $provider): bool
*/
protected function bootServiceProvider(Serviceable|Bootable $provider): void
{
+ $provider->callBootingCallbacks();
+
if (method_exists(object_or_class: $provider, method: 'boot')) {
$this->execute(callableOrMethodStr: [$provider, 'boot']);
}
+
+ $provider->callBootedCallbacks();
}
/**
@@ -496,7 +481,7 @@ public function booted(callable $callback): void
{
$this->bootedCallbacks[] = $callback;
- if ($this->isBooted()) {
+ if ($this->booted) {
$callback($this);
}
}
@@ -521,16 +506,14 @@ protected function fireAppCallbacks(array &$callbacks): void
/**
* Mark the particular ServiceProvider as having been registered.
*
- * @param string|null $registered
* @param Serviceable|Bootable $serviceProvider
* @return void
*/
- protected function markServiceProviderAsRegistered(
- string|null $registered,
- Serviceable|Bootable $serviceProvider
- ): void {
- $this->serviceProviders[$registered] = $serviceProvider;
- $this->serviceProvidersRegistered[$registered] = true;
+ protected function markServiceProviderAsRegistered(Serviceable|Bootable $serviceProvider): void
+ {
+ $class = get_class($serviceProvider);
+ $this->serviceProviders[$class] = $serviceProvider;
+ $this->serviceProvidersRegistered[$class] = true;
}
/**
@@ -596,7 +579,7 @@ public function withAppPath(string $appPath): self
*/
public function path(): string
{
- return $this->appPath ?: $this->basePath . self::DS . 'app';
+ return $this->appPath ?: $this->basePath . self::DS . 'App';
}
/**
@@ -619,6 +602,16 @@ public function bootStrapPath(): string
return $this->basePath . self::DS . 'bootstrap';
}
+ /**
+ * Get the path to the service provider list in the bootstrap directory.
+ *
+ * @return string
+ */
+ public function getBootstrapProvidersPath(): string
+ {
+ return $this->bootstrapPath() . self::DS . 'providers.php';
+ }
+
/**
* Get the path to the application's "config" directory.
*
@@ -711,11 +704,6 @@ public function withControllerNamespace(string $namespace): void
$this->controllerNamespace = $namespace;
}
- public function getControllerNamespace(): string
- {
- return $this->controllerNamespace;
- }
-
public function withBaseMiddlewares(array $middlewares): void
{
$this->baseMiddlewares = $middlewares;
@@ -773,26 +761,23 @@ protected function coreAliases(): array
\Psr\Container\ContainerInterface::class => self::class,
\Qubus\Injector\ServiceContainer::class => self::class,
\Psr\Http\Message\ServerRequestInterface::class => \Qubus\Http\ServerRequest::class,
- \Psr\Http\Message\ServerRequestFactoryInterface::class => \Qubus\Http\ServerRequestFactory::class,
+ \Psr\Http\Message\ServerRequestFactoryInterface::class => \Qubus\Http\Factories\Psr17Factory::class,
\Psr\Http\Message\RequestInterface::class => \Qubus\Http\Request::class,
+ \Psr\Http\Message\RequestFactoryInterface::class => \Qubus\Http\Factories\Psr17Factory::class,
\Psr\Http\Server\RequestHandlerInterface::class => \Relay\Runner::class,
\Psr\Http\Message\ResponseInterface::class => \Qubus\Http\Response::class,
- \Psr\Http\Message\ResponseFactoryInterface::class => \Laminas\Diactoros\ResponseFactory::class,
+ \Psr\Http\Message\ResponseFactoryInterface::class => \Qubus\Http\Factories\Psr17Factory::class,
\Psr\Cache\CacheItemInterface::class => \Qubus\Cache\Psr6\Item::class,
\Psr\Cache\CacheItemPoolInterface::class => \Qubus\Cache\Psr6\ItemPool::class,
\Qubus\Cache\Psr6\TaggableCacheItem::class => \Qubus\Cache\Psr6\TaggablePsr6ItemAdapter::class,
\Qubus\Cache\Psr6\TaggableCacheItemPool::class => \Qubus\Cache\Psr6\TaggablePsr6PoolAdapter::class,
\Qubus\Config\Path\Path::class => \Qubus\Config\Path\ConfigPath::class,
\Qubus\Config\ConfigContainer::class => \Qubus\Config\Collection::class,
- \Qubus\EventDispatcher\EventDispatcher::class => \Qubus\EventDispatcher\Dispatcher::class,
\Qubus\Mail\Mailer::class => \Codefy\Framework\Support\CodefyMailer::class,
'mailer' => Mailer::class,
'dir.path' => \Codefy\Framework\Support\Paths::class,
'container' => self::class,
'codefy' => self::class,
- \Qubus\Routing\Interfaces\Collector::class => \Qubus\Routing\Route\RouteCollector::class,
- 'router' => \Qubus\Routing\Router::class,
- \Codefy\Framework\Contracts\Kernel::class => \Codefy\Framework\Http\Kernel::class,
\Codefy\Framework\Contracts\RoutingController::class => \Codefy\Framework\Http\BaseController::class,
\League\Flysystem\FilesystemOperator::class => \Qubus\FileSystem\FileSystem::class,
\League\Flysystem\FilesystemAdapter::class => \Qubus\FileSystem\Adapter\LocalFlysystemAdapter::class,
@@ -815,19 +800,49 @@ protected function coreAliases(): array
];
}
+ /**
+ * @throws \Exception
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ /** @var Kernel $kernel */
+ $kernel = $this->make(name: Kernel::class);
+
+ return $kernel->handle($request);
+ }
+
+ public function handleRequest(?ServerRequestInterface $request = null): void
+ {
+ /** @var Kernel $kernel */
+ $kernel = $this->make(name: Kernel::class);
+
+ $kernel->boot($request);
+ }
+
/**
* Load environment file(s).
*
+ * @param string $basePath
* @return void
+ * @throws TypeException
+ * @throws ReflectionException
*/
- private function loadEnvironment(): void
+ private static function loadEnvironment(string $basePath): void
{
- $dotenv = Dotenv::createImmutable(
- paths: $this->basePath(),
- names: ['.env','.env.local','.env.staging','.env.development','.env.production'],
- shortCircuit: false
- );
- $dotenv->safeLoad();
+ if (self::$encryptedEnv) {
+ try {
+ SecureEnv::parse(inputFile: $basePath . '/.env.enc', keyFile: $basePath . '/.enc.key');
+ } catch (BadFormatException | EnvironmentIsBrokenException | WrongKeyOrModifiedCiphertextException $e) {
+ FileLoggerFactory::getLogger()->error($e->getMessage());
+ }
+ } else {
+ $dotenv = Dotenv::createImmutable(
+ paths: $basePath,
+ names: ['.env','.env.local','.env.staging','.env.development','.env.production'],
+ shortCircuit: false
+ );
+ $dotenv->safeLoad();
+ }
}
public function __get(mixed $name)
@@ -879,13 +894,16 @@ public function isDevelopment(): bool
}
/**
- * Configure a new CodefyPHP application instance.
+ * Create a new CodefyPHP application instance.
*
* @throws TypeException
+ * @throws ReflectionException
*/
- public static function configure(array $config): ApplicationBuilder
+ public static function create(array $config): ApplicationBuilder
{
- return new ApplicationBuilder(new self($config));
+ return new ApplicationBuilder(new self($config))
+ ->withKernels()
+ ->withProviders();
}
/**
@@ -893,6 +911,7 @@ public static function configure(array $config): ApplicationBuilder
*
* @return static
* @throws TypeException
+ * @throws ReflectionException
*/
public static function getInstance(?string $path = null): self
{
@@ -910,6 +929,74 @@ public static function getInstance(?string $path = null): self
);
}
+ self::loadEnvironment($basePath);
+
return self::$APP;
}
+
+ //phpcs:disable
+ public private(set) ServerRequestInterface $request {
+ get => $this->request ?? $this->make(name: ServerRequestInterface::class);
+ }
+
+ public private(set) ResponseInterface $response {
+ get => $this->response ?? $this->make(name: ResponseInterface::class);
+ }
+
+ public private(set) Assets $assets {
+ get => $this->assets ?? $this->make(name: 'assets.group.default');
+ }
+
+ public private(set) Mailer $mailer {
+ get => $this->mailer ?? $this->make(name: Mailer::class);
+ }
+
+ public private(set) PhpSession $session {
+ get => $this->session ?? $this->make(name: PhpSession::class);
+ }
+
+ public private(set) Flash $flash {
+ get => $this->flash ?? $this->make(name: Flash::class);
+ }
+
+ public private(set) EventDispatcherInterface $event {
+ get => $this->event ?? $this->make(name: EventDispatcherInterface::class);
+ }
+
+ public private(set) HttpCookieFactory $httpCookie {
+ get => $this->httpCookie ?? $this->make(name: HttpCookieFactory::class);
+ }
+
+ public private(set) LocalStorage $localStorage {
+ get => $this->localStorage ?? $this->make(name: LocalStorage::class);
+ }
+
+ public private(set) ConfigContainer $configContainer {
+ get => $this->configContainer ?? $this->make(name: ConfigContainer::class);
+ }
+
+ public private(set) PipelineBuilder $pipeline {
+ get => $this->pipeline ?? $this->make(name: PipelineBuilder::class);
+ }
+
+ public private(set) Observer $hook {
+ get => $this->hook ?? $this->make(name: Observer::class);
+ }
+
+ public private(set) StringHelper $string {
+ get => $this->string ?? $this->make(name: StringHelper::class);
+ }
+
+ public private(set) ArrayHelper $array {
+ get => $this->array ?? $this->make(name: ArrayHelper::class);
+ }
+
+ public private(set) Router $router {
+ get => $this->router ?? $this->make(name: Router::class);
+ }
+
+ public private(set) Paths $path {
+ get => $this->path ?? $this->make(name: Paths::class);
+ }
+ //phpcs:disable
}
diff --git a/Auth/Auth.php b/src/Auth/Auth.php
similarity index 79%
rename from Auth/Auth.php
rename to src/Auth/Auth.php
index 0e89211..1e560e9 100644
--- a/Auth/Auth.php
+++ b/src/Auth/Auth.php
@@ -11,6 +11,8 @@
use Psr\Http\Message\ServerRequestInterface;
use Qubus\Config\ConfigContainer;
use Qubus\Exception\Exception;
+use Qubus\Http\Factories\JsonResponseFactory;
+use Qubus\Http\Factories\RedirectResponseFactory;
use Qubus\Http\Session\SessionEntity;
class Auth implements Sentinel
@@ -49,18 +51,15 @@ public function authenticate(ServerRequestInterface $request): ?SessionEntity
}
/**
- * @throws Exception
+ * @throws \Exception
*/
public function unauthorized(ServerRequestInterface $request): ResponseInterface
{
- return $this->responseFactory
- ->createResponse(code: 302)
- ->withHeader(
- name: 'Location',
- value: $this->configContainer->getConfigKey(
- key: 'auth.http_redirect',
- default: $this->configContainer->getConfigKey(key: 'auth.login_url')
- )
- );
+ $location = $this->configContainer->getConfigKey(key: 'auth.http_redirect');
+ if (!empty($location)) {
+ return RedirectResponseFactory::create($location);
+ }
+
+ return JsonResponseFactory::create(data: 'Forbidden.', status: 403);
}
}
diff --git a/Auth/Rbac/Entity/AssertionRule.php b/src/Auth/Rbac/Entity/AssertionRule.php
similarity index 100%
rename from Auth/Rbac/Entity/AssertionRule.php
rename to src/Auth/Rbac/Entity/AssertionRule.php
diff --git a/Auth/Rbac/Entity/Permission.php b/src/Auth/Rbac/Entity/Permission.php
similarity index 75%
rename from Auth/Rbac/Entity/Permission.php
rename to src/Auth/Rbac/Entity/Permission.php
index 5fd6638..7efcda7 100644
--- a/Auth/Rbac/Entity/Permission.php
+++ b/src/Auth/Rbac/Entity/Permission.php
@@ -6,15 +6,11 @@
interface Permission
{
- /**
- * @return string
- */
- public function getName(): string;
+ //phpcs:disable
+ public string $name { get; }
- /**
- * @return string
- */
- public function getDescription(): string;
+ public string $description { get; }
+ //phpcs:enable
/**
* @param Permission $permission
@@ -45,5 +41,5 @@ public function getRuleClass(): ?string;
* @param array|null $params
* @return bool
*/
- public function checkAccess(array $params = null): bool;
+ public function checkAccess(?array $params = null): bool;
}
diff --git a/Auth/Rbac/Entity/RbacPermission.php b/src/Auth/Rbac/Entity/RbacPermission.php
similarity index 84%
rename from Auth/Rbac/Entity/RbacPermission.php
rename to src/Auth/Rbac/Entity/RbacPermission.php
index d33704f..5638e24 100644
--- a/Auth/Rbac/Entity/RbacPermission.php
+++ b/src/Auth/Rbac/Entity/RbacPermission.php
@@ -16,40 +16,30 @@ class RbacPermission implements Permission
protected array $childrenNames = [];
protected ?string $ruleClass = '';
+ //phpcs:disable
/**
- * @param string $permissionName
+ * @param string $name
* @param string $description
* @param StorageResource $rbacStorageCollection
*/
public function __construct(
- protected string $permissionName,
- protected string $description,
+ public private(set) string $name {
+ get => $this->name;
+ },
+ public private(set) string $description {
+ get => $this->description;
+ },
protected StorageResource $rbacStorageCollection
) {
}
-
- /**
- * @return string
- */
- public function getName(): string
- {
- return $this->permissionName;
- }
-
- /**
- * @return string
- */
- public function getDescription(): string
- {
- return $this->description;
- }
+ //phpcs.enable
/**
* @param Permission $permission
*/
public function addChild(Permission $permission): void
{
- $this->childrenNames[$permission->getName()] = true;
+ $this->childrenNames[$permission->name] = true;
}
/**
diff --git a/Auth/Rbac/Entity/RbacRole.php b/src/Auth/Rbac/Entity/RbacRole.php
similarity index 85%
rename from Auth/Rbac/Entity/RbacRole.php
rename to src/Auth/Rbac/Entity/RbacRole.php
index 86f48af..9c24972 100644
--- a/Auth/Rbac/Entity/RbacRole.php
+++ b/src/Auth/Rbac/Entity/RbacRole.php
@@ -12,43 +12,32 @@
class RbacRole implements Role
{
protected array $childrenNames = [];
-
protected array $permissionNames = [];
+ //phpcs:disable
/**
- * @param string $roleName
+ * @param string $name
* @param string $description
* @param StorageResource $rbacStorageCollection
*/
public function __construct(
- protected string $roleName,
- protected string $description,
+ public private(set) string $name {
+ get => $this->name;
+ },
+ public private(set) string $description {
+ get => $this->description;
+ },
protected StorageResource $rbacStorageCollection
) {
}
-
- /**
- * @return string
- */
- public function getName(): string
- {
- return $this->roleName;
- }
-
- /**
- * @return string
- */
- public function getDescription(): string
- {
- return $this->description;
- }
+ //phpcs:enable
/**
* @param Role $role
*/
public function addChild(Role $role): void
{
- $this->childrenNames[$role->getName()] = true;
+ $this->childrenNames[$role->name] = true;
}
/**
@@ -77,7 +66,7 @@ public function getChildren(): array
*/
public function addPermission(Permission $permission): void
{
- $this->permissionNames[$permission->getName()] = true;
+ $this->permissionNames[$permission->name] = true;
}
/**
@@ -137,7 +126,7 @@ public function checkAccess(string $permissionName, ?array $params = null): bool
protected function collectChildrenPermissions(Permission $permission, &$result): void
{
foreach ($permission->getChildren() as $childPermission) {
- $childPermissionName = $childPermission->getName();
+ $childPermissionName = $childPermission->name;
if (!isset($result[$childPermissionName])) {
$result[$childPermissionName] = $childPermission;
$this->collectChildrenPermissions($childPermission, $result);
diff --git a/Auth/Rbac/Entity/Role.php b/src/Auth/Rbac/Entity/Role.php
similarity index 79%
rename from Auth/Rbac/Entity/Role.php
rename to src/Auth/Rbac/Entity/Role.php
index ef0685e..da53754 100644
--- a/Auth/Rbac/Entity/Role.php
+++ b/src/Auth/Rbac/Entity/Role.php
@@ -8,15 +8,11 @@
interface Role
{
- /**
- * @return string
- */
- public function getName(): string;
+ //phpcs:disable
+ public string $name { get; }
- /**
- * @return string
- */
- public function getDescription(): string;
+ public string $description { get; }
+ //phpcs:enable
/**
* @param Role $role
@@ -55,5 +51,5 @@ public function getPermissions(bool $withChildren = false): array;
* @return bool
* @throws SentinelException
*/
- public function checkAccess(string $permissionName, array $params = null): bool;
+ public function checkAccess(string $permissionName, ?array $params = null): bool;
}
diff --git a/Auth/Rbac/Exception/SentinelException.php b/src/Auth/Rbac/Exception/SentinelException.php
similarity index 100%
rename from Auth/Rbac/Exception/SentinelException.php
rename to src/Auth/Rbac/Exception/SentinelException.php
diff --git a/Auth/Rbac/Guard.php b/src/Auth/Rbac/Guard.php
similarity index 85%
rename from Auth/Rbac/Guard.php
rename to src/Auth/Rbac/Guard.php
index bbfc20c..826aeaa 100644
--- a/Auth/Rbac/Guard.php
+++ b/src/Auth/Rbac/Guard.php
@@ -20,13 +20,17 @@ public function addRole(string $name, string $description = ''): Role;
*/
public function addPermission(string $name, string $description = ''): Permission;
- public function getRoles(): array;
+ //phpcs:disable
+ public array $roles { &get; }
+ //phpcs:enable
public function getRole(string $name): Role|null;
public function deleteRole(string $name): void;
- public function getPermissions(): array;
+ //phpcs:disable
+ public array $permissions { &get; }
+ //phpcs:enable
public function getPermission(string $name): Permission|null;
diff --git a/Auth/Rbac/Rbac.php b/src/Auth/Rbac/Rbac.php
similarity index 90%
rename from Auth/Rbac/Rbac.php
rename to src/Auth/Rbac/Rbac.php
index ff1a96e..69cdbd8 100644
--- a/Auth/Rbac/Rbac.php
+++ b/src/Auth/Rbac/Rbac.php
@@ -34,13 +34,14 @@ public function addPermission(string $name, string $description = ''): Permissio
return $this->storageResource->addPermission($name, $description);
}
+ //phpcs:disable
/**
* @return Role[]
*/
- public function getRoles(): array
- {
- return $this->storageResource->getRoles();
+ public array $roles = [] {
+ &get => $this->roles;
}
+ //phpcs:enable
/**
* @param string $name
@@ -59,13 +60,14 @@ public function deleteRole(string $name): void
$this->storageResource->deleteRole($name);
}
+ //phpcs:disable
/**
* @return Permission[]
*/
- public function getPermissions(): array
- {
- return $this->storageResource->getPermissions();
+ public array $permissions = [] {
+ &get => $this->permissions;
}
+ //phpcs:enable
/**
* @param string $name
diff --git a/src/Auth/Rbac/RbacLoader.php b/src/Auth/Rbac/RbacLoader.php
new file mode 100644
index 0000000..10a8801
--- /dev/null
+++ b/src/Auth/Rbac/RbacLoader.php
@@ -0,0 +1,82 @@
+configContainer->getConfigKey(key: 'rbac.roles'))) {
+ $rolesConfig = (array) $this->configContainer->getConfigKey(key: 'rbac.roles', default: []);
+ $this->addRoles($rolesConfig);
+ }
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function initRbacPermissions(): void
+ {
+ if (!is_null__($this->configContainer->getConfigKey(key: 'rbac.permissions'))) {
+ $permissionsConfig = (array) $this->configContainer->getConfigKey(key: 'rbac.permissions', default: []);
+ $this->addPermissions($permissionsConfig);
+ }
+ }
+
+ private function addRoles(array $rolesConfig, ?Role $parent = null): void
+ {
+ foreach ($rolesConfig as $name => $config) {
+ $role = $this->rbac->addRole(name: $name, description: $config['description'] ?? '');
+ if (!empty($config['permissions'])) {
+ foreach ((array) $config['permissions'] as $permissionName) {
+ if ($permission = $this->rbac->getPermission(name: $permissionName)) {
+ $role->addPermission($permission);
+ } else {
+ throw new RuntimeException(message: sprintf('Permission not found: %s', $permissionName));
+ }
+ }
+ }
+ }
+
+ $parent?->addChild($role);
+
+ if (!empty($config['roles'])) {
+ $this->addRoles(rolesConfig: $config['roles'], parent: $role);
+ }
+ }
+
+ /**
+ * @throws SentinelException
+ */
+ private function addPermissions(array $permissionsConfig, ?Permission $parent = null): void
+ {
+ foreach ($permissionsConfig as $name => $config) {
+ $permission = $this->rbac->addPermission(name: $name, description: $config['description'] ?? '');
+
+ $parent?->addChild($permission);
+
+ if (!empty($config['permissions'])) {
+ $this->addPermissions(permissionsConfig: $config['permissions'], parent: $permission);
+ }
+ }
+ }
+}
diff --git a/Auth/Rbac/Resource/BaseStorageResource.php b/src/Auth/Rbac/Resource/BaseStorageResource.php
similarity index 78%
rename from Auth/Rbac/Resource/BaseStorageResource.php
rename to src/Auth/Rbac/Resource/BaseStorageResource.php
index 095d1a0..2531027 100644
--- a/Auth/Rbac/Resource/BaseStorageResource.php
+++ b/src/Auth/Rbac/Resource/BaseStorageResource.php
@@ -12,9 +12,15 @@
abstract class BaseStorageResource implements StorageResource
{
- protected array $roles = [];
+ //phpcs:disable
+ public protected(set) array $roles = [] {
+ &get => $this->roles;
+ }
- protected array $permissions = [];
+ public protected(set) array $permissions = [] {
+ &get => $this->permissions;
+ }
+ //phpcs:enable
/**
* @throws SentinelException
@@ -24,7 +30,7 @@ public function addRole(string $name, string $description = ''): Role
if (isset($this->roles[$name])) {
throw new SentinelException(message: 'Role already exists.');
}
- $role = new RbacRole(roleName: $name, description: $description, rbacStorageCollection: $this);
+ $role = new RbacRole(name: $name, description: $description, rbacStorageCollection: $this);
$this->roles[$name] = $role;
return $role;
}
@@ -36,7 +42,7 @@ public function addPermission(string $name, string $description = ''): Permissio
}
$permission = new RbacPermission(
- permissionName: $name,
+ name: $name,
description: $description,
rbacStorageCollection: $this
);
@@ -45,11 +51,6 @@ public function addPermission(string $name, string $description = ''): Permissio
return $permission;
}
- public function getRoles(): array
- {
- return $this->roles;
- }
-
public function getRole(string $name): ?Role
{
return $this->roles[$name] ?? null;
@@ -59,16 +60,11 @@ public function deleteRole(string $name): void
{
unset($this->roles[$name]);
- foreach ($this->getRoles() as $role) {
+ foreach ($this->roles as $role) {
$role->removeChild($name);
}
}
- public function getPermissions(): array
- {
- return $this->permissions;
- }
-
public function getPermission(string $name): ?Permission
{
return $this->permissions[$name] ?? null;
@@ -78,11 +74,11 @@ public function deletePermission(string $name): void
{
unset($this->permissions[$name]);
- foreach ($this->getRoles() as $role) {
+ foreach ($this->roles as $role) {
$role->removePermission(permissionName: $name);
}
- foreach ($this->getPermissions() as $permission) {
+ foreach ($this->permissions as $permission) {
$permission->removeChild(permissionName: $name);
}
}
diff --git a/src/Auth/Rbac/Resource/FileResource.php b/src/Auth/Rbac/Resource/FileResource.php
new file mode 100644
index 0000000..edfe93c
--- /dev/null
+++ b/src/Auth/Rbac/Resource/FileResource.php
@@ -0,0 +1,151 @@
+file = $file;
+ }
+
+ /**
+ * @throws SentinelException
+ * @throws FilesystemException|TypeException
+ */
+ public function load(): void
+ {
+ $this->clear();
+
+ if (!file_exists($this->file) || (!$data = LocalStorage::disk()->read(json_decode($this->file, true)))) {
+ $data = [];
+ }
+
+ $this->restorePermissions($data['permissions'] ?? []);
+ $this->restoreRoles($data['roles'] ?? []);
+ }
+
+ /**
+ * @throws FilesystemException
+ * @throws TypeException|FilesystemException
+ */
+ public function save(): void
+ {
+ $data = [
+ 'roles' => [],
+ 'permissions' => [],
+ ];
+ foreach ($this->roles as $role) {
+ $data['roles'][$role->name] = $this->roleToRow($role);
+ }
+ foreach ($this->permissions as $permission) {
+ $data['permissions'][$permission->name] = $this->permissionToRow($permission);
+ }
+
+ LocalStorage::disk()->write($this->file, json_encode(value: $data, flags: JSON_PRETTY_PRINT));
+ }
+
+ protected function roleToRow(Role $role): array
+ {
+ $result = [];
+ $result['name'] = $role->name;
+ $result['description'] = $role->description;
+ $childrenNames = [];
+ foreach ($role->getChildren() as $child) {
+ $childrenNames[] = $child->name;
+ }
+ $result['children'] = $childrenNames;
+ $permissionNames = [];
+ foreach ($role->getPermissions() as $permission) {
+ $permissionNames[] = $permission->name;
+ }
+ $result['permissions'] = $permissionNames;
+ return $result;
+ }
+
+ protected function permissionToRow(Permission $permission): array
+ {
+ $result = [];
+ $result['name'] = $permission->name;
+ $result['description'] = $permission->description;
+ $childrenNames = [];
+ foreach ($permission->getChildren() as $child) {
+ $childrenNames[] = $child->name;
+ }
+ $result['children'] = $childrenNames;
+ $result['ruleClass'] = $permission->getRuleClass();
+ return $result;
+ }
+
+ /**
+ * @throws SentinelException
+ */
+ protected function restorePermissions(array $permissionsData): void
+ {
+ /** @var string[][] $permChildrenNames */
+ $permChildrenNames = [];
+
+ foreach ($permissionsData as $pData) {
+ $permission = $this->addPermission($pData['name'] ?? '', $pData['description'] ?? '');
+ $permission->setRuleClass($pData['ruleClass'] ?? '');
+ $permChildrenNames[$permission->name] = $pData['children'] ?? [];
+ }
+
+ foreach ($permChildrenNames as $permissionName => $childrenNames) {
+ foreach ($childrenNames as $childName) {
+ $permission = $this->getPermission($permissionName);
+ $child = $this->getPermission($childName);
+ if ($permission && $child) {
+ $permission->addChild($child);
+ }
+ }
+ }
+ }
+
+ /**
+ * @throws SentinelException
+ */
+ protected function restoreRoles($rolesData): void
+ {
+ /** @var string[][] $rolesChildrenNames */
+ $rolesChildrenNames = [];
+
+ foreach ($rolesData as $rData) {
+ $role = $this->addRole($rData['name'] ?? '', $rData['description'] ?? '');
+ $rolesChildrenNames[$role->name] = $rData['children'] ?? [];
+ $permissionNames = $rData['permissions'] ?? [];
+ foreach ($permissionNames as $permissionName) {
+ if ($permission = $this->getPermission($permissionName)) {
+ $role->addPermission($permission);
+ }
+ }
+ }
+
+ foreach ($rolesChildrenNames as $roleName => $childrenNames) {
+ foreach ($childrenNames as $childName) {
+ $role = $this->getRole($roleName);
+ $child = $this->getRole($childName);
+ if ($role && $child) {
+ $role->addChild($child);
+ }
+ }
+ }
+ }
+}
diff --git a/Auth/Rbac/Resource/StorageResource.php b/src/Auth/Rbac/Resource/StorageResource.php
similarity index 100%
rename from Auth/Rbac/Resource/StorageResource.php
rename to src/Auth/Rbac/Resource/StorageResource.php
diff --git a/Auth/Repository/AuthUserRepository.php b/src/Auth/Repository/AuthUserRepository.php
similarity index 73%
rename from Auth/Repository/AuthUserRepository.php
rename to src/Auth/Repository/AuthUserRepository.php
index a28e0b4..98ad99b 100644
--- a/Auth/Repository/AuthUserRepository.php
+++ b/src/Auth/Repository/AuthUserRepository.php
@@ -5,6 +5,7 @@
namespace Codefy\Framework\Auth\Repository;
use Qubus\Http\Session\SessionEntity;
+use SensitiveParameter;
interface AuthUserRepository
{
@@ -17,5 +18,5 @@ interface AuthUserRepository
* @param string|null $password
* @return SessionEntity|null
*/
- public function authenticate(string $credential, ?string $password = null): ?SessionEntity;
+ public function authenticate(string $credential, #[SensitiveParameter] ?string $password = null): ?SessionEntity;
}
diff --git a/Auth/Repository/PdoRepository.php b/src/Auth/Repository/PdoRepository.php
similarity index 51%
rename from Auth/Repository/PdoRepository.php
rename to src/Auth/Repository/PdoRepository.php
index ba69258..15a5ccb 100644
--- a/Auth/Repository/PdoRepository.php
+++ b/src/Auth/Repository/PdoRepository.php
@@ -4,17 +4,19 @@
namespace Codefy\Framework\Auth\Repository;
+use Codefy\Framework\Auth\UserSession;
use Codefy\Framework\Support\Password;
-use PDO;
use Qubus\Config\ConfigContainer;
use Qubus\Exception\Exception;
+use Qubus\Expressive\Connection;
use Qubus\Http\Session\SessionEntity;
+use SensitiveParameter;
use function sprintf;
class PdoRepository implements AuthUserRepository
{
- public function __construct(private PDO $pdo, protected ConfigContainer $config)
+ public function __construct(private Connection $connection, protected ConfigContainer $config)
{
}
@@ -22,7 +24,7 @@ public function __construct(private PDO $pdo, protected ConfigContainer $config)
* @inheritdoc
* @throws Exception
*/
- public function authenticate(string $credential, #[\SensitiveParameter] ?string $password = null): ?SessionEntity
+ public function authenticate(string $credential, #[SensitiveParameter] ?string $password = null): ?SessionEntity
{
$fields = $this->config->getConfigKey(key: 'auth.pdo.fields');
@@ -32,7 +34,7 @@ public function authenticate(string $credential, #[\SensitiveParameter] ?string
$fields['identity']
);
- $stmt = $this->pdo->prepare(query: $sql);
+ $stmt = $this->connection->pdo->prepare($sql);
if (false === $stmt) {
return null;
}
@@ -48,31 +50,9 @@ public function authenticate(string $credential, #[\SensitiveParameter] ?string
$passwordHash = (string) ($result->{$fields['password']} ?? '');
if (Password::verify(password: $password ?? '', hash: $passwordHash)) {
- $user = new class () implements SessionEntity {
- public ?string $token = null;
- public ?string $role = null;
-
- public function withToken(?string $token = null): self
- {
- $this->token = $token;
- return $this;
- }
-
- public function withRole(?string $role = null): self
- {
- $this->role = $role;
- return $this;
- }
-
- public function isEmpty(): bool
- {
- return !empty($this->token) && !empty($this->role);
- }
- };
-
+ $user = new UserSession();
$user
- ->withToken($result->token)
- ->withRole($result->role);
+ ->withToken($result->token);
return $user;
}
diff --git a/Auth/Sentinel.php b/src/Auth/Sentinel.php
similarity index 100%
rename from Auth/Sentinel.php
rename to src/Auth/Sentinel.php
diff --git a/Auth/Traits/BadPropertyCallException.php b/src/Auth/Traits/BadPropertyCallException.php
similarity index 100%
rename from Auth/Traits/BadPropertyCallException.php
rename to src/Auth/Traits/BadPropertyCallException.php
diff --git a/Auth/Traits/ImmutableAware.php b/src/Auth/Traits/ImmutableAware.php
similarity index 100%
rename from Auth/Traits/ImmutableAware.php
rename to src/Auth/Traits/ImmutableAware.php
diff --git a/Auth/UserSession.php b/src/Auth/UserSession.php
similarity index 58%
rename from Auth/UserSession.php
rename to src/Auth/UserSession.php
index afbf314..3c94c8e 100644
--- a/Auth/UserSession.php
+++ b/src/Auth/UserSession.php
@@ -8,19 +8,12 @@
class UserSession implements SessionEntity
{
- public ?string $token = null;
-
- public ?string $role = null;
+ public protected(set) ?string $token = null;
public function withToken(?string $token = null): self
{
$this->token = $token;
- return $this;
- }
- public function withRole(?string $role = null): self
- {
- $this->role = $role;
return $this;
}
@@ -29,14 +22,10 @@ public function clear(): void
if (!empty($this->token)) {
unset($this->token);
}
-
- if (!empty($this->role)) {
- unset($this->role);
- }
}
public function isEmpty(): bool
{
- return empty($this->token) && empty($this->role);
+ return empty($this->token);
}
}
diff --git a/Bootstrap/BootProviders.php b/src/Bootstrap/BootProviders.php
similarity index 100%
rename from Bootstrap/BootProviders.php
rename to src/Bootstrap/BootProviders.php
diff --git a/src/Bootstrap/RegisterProviders.php b/src/Bootstrap/RegisterProviders.php
new file mode 100644
index 0000000..becfb40
--- /dev/null
+++ b/src/Bootstrap/RegisterProviders.php
@@ -0,0 +1,109 @@
+mergeAdditionalProviders(app: $app);
+ $app->registerConfiguredServiceProviders();
+ }
+
+ /**
+ * Merge additional configured providers into the configuration.
+ *
+ * @param Application $app
+ * @return void
+ * @throws Exception
+ */
+ protected function mergeAdditionalProviders(Application $app): void
+ {
+ if (
+ self::$bootstrapProviderPath &&
+ file_exists(self::$bootstrapProviderPath)
+ ) {
+ $packageProviders = require self::$bootstrapProviderPath;
+
+ foreach ($packageProviders as $index => $provider) {
+ if (! class_exists($provider)) {
+ unset($packageProviders[$index]);
+ }
+ }
+ }
+
+ /** @var ConfigContainer $config */
+ $config = $app->make(name: 'codefy.config');
+
+ $arrayMerge = array_unique(
+ array: array_merge(
+ self::$merge,
+ $config->getConfigKey(
+ key: 'app.providers',
+ default: CodefyServiceProvider::defaultProviders()->toArray()
+ ),
+ array_values($packageProviders ?? []),
+ )
+ );
+
+ self::$providers = array_merge(self::$providers, $arrayMerge);
+
+ $config->setConfigKey(key: 'app', value: ['providers' => $arrayMerge,]);
+ }
+
+ /**
+ * Merge the given providers into the provider configuration
+ * before registration.
+ *
+ * @param array $providers
+ * @param string|null $bootstrapProviderPath
+ * @return void
+ */
+ public static function merge(array $providers, ?string $bootstrapProviderPath = null): void
+ {
+ self::$bootstrapProviderPath = $bootstrapProviderPath;
+
+ self::$merge = array_values(
+ array: array_filter(
+ array: array_unique(
+ array: array_merge(self::$merge, $providers)
+ )
+ )
+ );
+ }
+
+ public static function flushState(): void
+ {
+ self::$bootstrapProviderPath = null;
+
+ self::$merge = [];
+ }
+}
diff --git a/src/Configuration/ApplicationBuilder.php b/src/Configuration/ApplicationBuilder.php
new file mode 100644
index 0000000..e45f726
--- /dev/null
+++ b/src/Configuration/ApplicationBuilder.php
@@ -0,0 +1,237 @@
+app->singleton(
+ \Codefy\Framework\Contracts\Http\Kernel::class,
+ fn() => $this->app->make(name: \Codefy\Framework\Http\Kernel::class)
+ );
+
+ $this->app->singleton(
+ key: \Codefy\Framework\Contracts\Console\Kernel::class,
+ value: fn() => $this->app->make(name: \Codefy\Framework\Console\ConsoleKernel::class)
+ );
+
+ return $this;
+ }
+
+ /**
+ * Register additional service providers.
+ *
+ * @param array $providers
+ * @param bool $withBootstrapProviders
+ * @return $this
+ * @throws TypeException
+ */
+ public function withProviders(array $providers = [], bool $withBootstrapProviders = true): self
+ {
+ RegisterProviders::merge(
+ providers: $providers,
+ bootstrapProviderPath: $withBootstrapProviders
+ ? $this->app->getBootstrapProvidersPath()
+ : null
+ );
+
+ foreach ($providers as $provider) {
+ $this->app->registerServiceProvider($provider);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Register an array of singletons that are resource intensive
+ * or are not called often.
+ *
+ * @param array $singletons
+ * @return $this
+ */
+ public function withSingletons(array $singletons = []): self
+ {
+ if (empty($singletons)) {
+ return $this;
+ }
+
+ foreach ($singletons as $key => $callable) {
+ $this->app->singleton($key, $callable);
+ };
+
+ return $this;
+ }
+
+ /**
+ * Register the routing services for the application.
+ *
+ * @param callable|Closure|null $using
+ * @param array|string|null $web
+ * @param array|null $class
+ * @param array|string|null $api
+ * @param string $apiPrefix
+ * @param callable|null $then
+ * @return $this
+ * @throws TypeException
+ */
+ public function withRouting(
+ callable|Closure|null $using = null,
+ array|string|null $web = null,
+ ?array $class = null,
+ array|string|null $api = null,
+ string $apiPrefix = 'api',
+ ?callable $then = null,
+ ): self {
+ if (
+ is_null__($using) &&
+ (is_string($web) || is_array($web) || is_string($api) || is_array($api)) ||
+ is_callable($then)
+ ) {
+ $using = $this->buildRoutingCallback($web, $api, $apiPrefix, $then);
+ }
+
+ RoutingServiceProvider::loadRoutesUsing($using);
+
+ $this->app->booting(function () {
+ $this->app->registerServiceProvider(serviceProvider: RoutingServiceProvider::class, force: true);
+ });
+
+ // Class-based routes
+ if (!is_null__($class) && is_array($class)) {
+ foreach ($class as $route) {
+ $this->app->execute([$route, 'handle']);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set whether environment variables
+ * should be encrypted.
+ *
+ * @param bool $bool Default: false.
+ * @return $this
+ */
+ public function withEncryptedEnv(bool $bool = false): self
+ {
+ $this->app::$encryptedEnv = $bool;
+
+ return $this;
+ }
+
+ /**
+ * Create the routing callback for the application.
+ *
+ * @param array|string|null $web
+ * @param array|string|null $api
+ * @param string $apiPrefix
+ * @param callable|null $then
+ * @return Closure
+ */
+ protected function buildRoutingCallback(
+ array|string|null $web = null,
+ array|string|null $api = null,
+ string $apiPrefix = 'api',
+ ?callable $then = null
+ ): Closure {
+ return function (Router $router) use ($web, $api, $apiPrefix, $then) {
+ $registrar = new RoutingRegistrar($router);
+
+ // Web routes
+ if ($web) {
+ $registrar->group($web);
+ }
+
+ // API routes
+ if ($api) {
+ /**
+ * API middleware filter.
+ */
+ $apiMiddleware = __observer()->filter->applyFilter('rest.api', ['api']);
+ $registrar->group($api, middleware: $apiMiddleware, prefix: $apiPrefix);
+ }
+
+ // Final callback
+ if (is_callable($then)) {
+ $then($router);
+ }
+ };
+ }
+
+ /**
+ * Register a callback to be invoked when the application's
+ * service providers are registered.
+ *
+ * @param callable $callback
+ * @return $this
+ */
+ public function registered(callable $callback): self
+ {
+ $this->app->registered(callback: $callback);
+
+ return $this;
+ }
+
+ /**
+ * Register a callback to be invoked when the application is "booting".
+ *
+ * @param callable $callback
+ * @return $this
+ */
+ public function booting(callable $callback): self
+ {
+ $this->app->booting($callback);
+
+ return $this;
+ }
+
+ /**
+ * Register a callback to be invoked when the application is "booted".
+ *
+ * @param callable $callback
+ * @return $this
+ */
+ public function booted(callable $callback): self
+ {
+ $this->app->booted($callback);
+
+ return $this;
+ }
+
+ /**
+ * Return the application instance.
+ *
+ * @return Application
+ */
+ public function return(): Application
+ {
+ return $this->app;
+ }
+}
diff --git a/src/Console/Commands/EncryptEnvCommand.php b/src/Console/Commands/EncryptEnvCommand.php
new file mode 100644
index 0000000..abd4c32
--- /dev/null
+++ b/src/Console/Commands/EncryptEnvCommand.php
@@ -0,0 +1,69 @@
+addOption(
+ name: 'env',
+ shortcut: null,
+ mode: InputOption::VALUE_REQUIRED,
+ description: 'Set the environment file to encrypt.'
+ );
+ }
+
+ public function handle(): int
+ {
+ $this->terminalRaw(string: 'Encrypting data and creating file . . .');
+
+ try {
+ $savedKeyString = file_get_contents(filename: base_path(path: '.enc.key'));
+ $key = Key::loadFromAsciiSafeString(saved_key_string: $savedKeyString);
+
+ if ($this->getOptions('env')) {
+ $file = base_path(path: sprintf('.env.%s', $this->getOptions('env')));
+ } else {
+ $file = base_path(path: '.env');
+ }
+
+ if (!file_exists($file)) {
+ $this->output->writeln(sprintf('File %s does not exist.', $file));
+ return ConsoleCommand::FAILURE;
+ }
+
+ File::encrypt($file, base_path(path: '.env.enc'), $key);
+ } catch (BadFormatException | EnvironmentIsBrokenException $e) {
+ return ConsoleCommand::FAILURE;
+ }
+
+ $this->terminalRaw(string: '.env.enc file created.');
+
+ // return value is important when using CI
+ // to fail the build when the command fails
+ // 0 = success, other values = fail
+ return ConsoleCommand::SUCCESS;
+ }
+}
diff --git a/src/Console/Commands/FlushPipelineCommand.php b/src/Console/Commands/FlushPipelineCommand.php
new file mode 100644
index 0000000..a8a785f
--- /dev/null
+++ b/src/Console/Commands/FlushPipelineCommand.php
@@ -0,0 +1,169 @@
+addOption(
+ name: 'group',
+ shortcut: '-g',
+ mode: InputOption::VALUE_REQUIRED,
+ description: 'Only flush the provided group'
+ )
+ ->addOption(
+ name: 'force',
+ shortcut: '-f',
+ mode: InputOption::VALUE_NONE,
+ description: 'Do not prompt for confirmation'
+ )
+ ->setDescription(description: 'Flush assets pipeline..')
+ ->setHelp(
+ help: <<asset:flush flushes the assets pipeline.
+php codex asset:flush
+EOT
+ );
+ }
+
+ /**
+ * @throws Exception
+ * @throws FilesystemException
+ */
+ public function handle(): int
+ {
+ // Get directories to purge
+ if (! $directories = $this->getPipelineDirectories()) {
+ $this->terminalError(string: 'The provided group does not exist');
+
+ return ConsoleCommand::FAILURE;
+ }
+
+ // Ask for confirmation
+ if (! $this->getOptions(key: 'force')) {
+ $this->terminalInfo(string: 'All content of the following directories will be deleted:');
+
+ foreach ($directories as $dir) {
+ $this->terminalComment($dir);
+ }
+
+ if (! $this->confirm(question: 'Do you want to continue?')) {
+ return ConsoleCommand::SUCCESS;
+ }
+ }
+
+ // Purge directories
+ $this->terminalComment(string: 'Flushing pipeline directories...');
+
+ foreach ($directories as $dir) {
+ $this->output->write(messages: "$dir ");
+
+ if ($this->purgeDir($dir)) {
+ $this->terminalInfo(string: 'OK');
+ } else {
+ $this->terminalError(string: 'ERROR');
+ }
+ }
+
+ $this->terminalComment(string: 'Done!');
+
+ return ConsoleCommand::SUCCESS;
+ }
+
+ /**
+ * Get the pipeline directories of the groups.
+ *
+ * @return array
+ * @throws Exception
+ */
+ protected function getPipelineDirectories(): array
+ {
+ // Parse configured groups
+ $config = $this->codefy->configContainer->getConfigKey(key: 'assets');
+
+ $groups = (isset($config['default'])) ? $config : ['default' => $config];
+
+ if (! empty($group = $this->getOptions(key: 'group'))) {
+ $groups = $this->codefy->array->subset(array: $groups, keys: [$group]);
+ }
+
+ // Parse pipeline directories of each group
+ $directories = [];
+
+ foreach ($groups as $group => $config) {
+ $pipelineDir = (isset($config['pipeline_dir'])) ? $config['pipeline_dir'] : 'min';
+ $publicDir = (isset($config['public_dir'])) ? $config['public_dir'] : public_path();
+ $publicDir = rtrim($publicDir, $this->codefy::DS);
+
+ $cssDir = (isset($config['css_dir'])) ? $config['css_dir'] : 'css';
+ $directories[] = implode($this->codefy::DS, [$publicDir, $cssDir, $pipelineDir]);
+
+ $jsDir = (isset($config['js_dir'])) ? $config['js_dir'] : 'js';
+ $directories[] = implode($this->codefy::DS, [$publicDir, $jsDir, $pipelineDir]);
+ }
+
+ // Clean results
+ $directories = array_unique($directories);
+ sort($directories);
+
+ return $directories;
+ }
+
+ /**
+ * Remove the contents of a given directory.
+ *
+ * @param string $directory
+ * @return bool
+ * @throws FilesystemException
+ */
+ protected function purgeDir(string $directory): bool
+ {
+ /** @var FileSystem $filesystem */
+ $filesystem = $this->codefy->make(name: 'filesystem.default');
+
+ if (! $filesystem->directoryExists($directory)) {
+ return true;
+ }
+
+ if (\Qubus\Support\Helpers\is_writable($directory)) {
+ $contents = $filesystem->listContents(location: $directory, deep: true)->toArray();
+
+ foreach ($contents as $item) {
+ // Check if the item is a file or a directory
+ if ($item->isFile()) {
+ $filesystem->delete($item->path());
+ } elseif ($item->isDir()) {
+ // Delete the directory and its contents (already handled by recursive listing)
+ // You might want to delete directories only after their contents are cleared
+ // Or, if you're deleting from the deepest level first, this will work.
+ // For a more controlled approach, you might want to sort the $contents
+ // to process deeper items first.
+ $filesystem->deleteDirectory($item->path());
+ }
+ }
+ $filesystem->deleteDirectory($directory);
+
+ return true;
+ }
+
+ $this->terminalError(sprintf('Directory "%s" is not writable.', $directory));
+
+ return false;
+ }
+}
diff --git a/src/Console/Commands/GenerateEncryptionKeyCommand.php b/src/Console/Commands/GenerateEncryptionKeyCommand.php
new file mode 100644
index 0000000..e91c2e0
--- /dev/null
+++ b/src/Console/Commands/GenerateEncryptionKeyCommand.php
@@ -0,0 +1,36 @@
+saveToAsciiSafeString();
+
+ $this->terminalRaw(string: sprintf(
+ 'Encryption Key: %s',
+ $key
+ ));
+
+ // return value is important when using CI
+ // to fail the build when the command fails
+ // 0 = success, other values = fail
+ return ConsoleCommand::SUCCESS;
+ }
+}
diff --git a/src/Console/Commands/GenerateEncryptionKeyFileCommand.php b/src/Console/Commands/GenerateEncryptionKeyFileCommand.php
new file mode 100644
index 0000000..6393ba8
--- /dev/null
+++ b/src/Console/Commands/GenerateEncryptionKeyFileCommand.php
@@ -0,0 +1,40 @@
+terminalRaw(string: 'Generating encryption key . . .');
+
+ $key = Key::createNewRandomKey()->saveToAsciiSafeString();
+
+ $this->terminalRaw(string: 'Generating encryption key file . . .');
+
+ file_put_contents(base_path(path: '.enc.key'), $key);
+
+ $this->terminalRaw(string: '.enc.key file created.');
+
+ // return value is important when using CI
+ // to fail the build when the command fails
+ // 0 = success, other values = fail
+ return ConsoleCommand::SUCCESS;
+ }
+}
diff --git a/Console/Commands/InitCommand.php b/src/Console/Commands/InitCommand.php
similarity index 100%
rename from Console/Commands/InitCommand.php
rename to src/Console/Commands/InitCommand.php
diff --git a/Console/Commands/MakeCommand.php b/src/Console/Commands/MakeCommand.php
similarity index 91%
rename from Console/Commands/MakeCommand.php
rename to src/Console/Commands/MakeCommand.php
index 5acddad..2184fda 100644
--- a/Console/Commands/MakeCommand.php
+++ b/src/Console/Commands/MakeCommand.php
@@ -15,7 +15,7 @@ class MakeCommand extends ConsoleCommand
{
use MakeCommandAware;
- protected const FILE_EXTENSION = '.php';
+ protected const string FILE_EXTENSION = '.php';
private array $errors = [];
@@ -30,12 +30,14 @@ class MakeCommand extends ConsoleCommand
protected string $help = 'Command which can generate a class file from a set of predefined stub files';
/* @var array Stubs */
- private const STUBS = [
+ private const array STUBS = [
'controller' => 'App\Infrastructure\Http\Controllers',
'repository' => 'App\Infrastructure\Persistence\Repository',
'provider' => 'App\Infrastructure\Providers',
'middleware' => 'App\Infrastructure\Http\Middleware',
'error' => 'App\Infrastructure\Errors',
+ 'command' => 'App\Application\Console\Commands',
+ 'route' => 'App\Infrastructure\Http\Routes',
];
protected array $args = [
@@ -67,7 +69,6 @@ public function handle(): int
return ConsoleCommand::SUCCESS;
} catch (MakeCommandFileAlreadyExistsException | TypeException | RuntimeException | FilesystemException $e) {
$this->terminalError(string: sprintf('%s', $e->getMessage()));
- } finally {
return ConsoleCommand::FAILURE;
}
}
diff --git a/Console/Commands/CheckCommand.php b/src/Console/Commands/MigrateCheckCommand.php
similarity index 97%
rename from Console/Commands/CheckCommand.php
rename to src/Console/Commands/MigrateCheckCommand.php
index 2693101..decef05 100644
--- a/Console/Commands/CheckCommand.php
+++ b/src/Console/Commands/MigrateCheckCommand.php
@@ -7,7 +7,7 @@
use Qubus\Exception\Exception;
use Symfony\Component\Console\Helper\Table;
-class CheckCommand extends PhpMigCommand
+class MigrateCheckCommand extends PhpMigCommand
{
protected string $name = 'migrate:check';
diff --git a/Console/Commands/MigrateCommand.php b/src/Console/Commands/MigrateCommand.php
similarity index 100%
rename from Console/Commands/MigrateCommand.php
rename to src/Console/Commands/MigrateCommand.php
diff --git a/Console/Commands/DownCommand.php b/src/Console/Commands/MigrateDownCommand.php
similarity index 97%
rename from Console/Commands/DownCommand.php
rename to src/Console/Commands/MigrateDownCommand.php
index 8208416..df67b85 100644
--- a/Console/Commands/DownCommand.php
+++ b/src/Console/Commands/MigrateDownCommand.php
@@ -8,7 +8,7 @@
use Qubus\Exception\Exception;
use Symfony\Component\Console\Input\InputArgument;
-class DownCommand extends PhpMigCommand
+class MigrateDownCommand extends PhpMigCommand
{
protected string $name = 'migrate:down';
diff --git a/Console/Commands/GenerateCommand.php b/src/Console/Commands/MigrateGenerateCommand.php
similarity index 93%
rename from Console/Commands/GenerateCommand.php
rename to src/Console/Commands/MigrateGenerateCommand.php
index 1a8efff..81d2dbd 100644
--- a/Console/Commands/GenerateCommand.php
+++ b/src/Console/Commands/MigrateGenerateCommand.php
@@ -11,9 +11,12 @@
use RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
+use function getcwd;
use function Qubus\Support\Helpers\is_writable;
+use function sprintf;
+use function str_replace;
-class GenerateCommand extends PhpMigCommand
+class MigrateGenerateCommand extends PhpMigCommand
{
protected string $name = 'migrate:generate';
@@ -122,7 +125,7 @@ public function handle(): int
declare(strict_types=1);
-use Codefy\Framework\Migration\Migration;
+use Qubus\Expressive\Migration\Migration;
class $className extends Migration
{
@@ -144,17 +147,13 @@ public function down(): void
}
if (false === file_put_contents(filename: $path, data: $contents)) {
- throw new RuntimeException(
- message: sprintf(
- 'The file "%s" could not be written to',
- $path
- )
- );
+ $this->terminalRaw(sprintf('The file "%s" could not be generated.', $path));
+ return ConsoleCommand::FAILURE;
}
$this->terminalRaw(
- string: '+f ' .
- '.' . str_replace(search: getcwd(), replace: '', subject: $path)
+ string: '+f ' .
+ '.' . str_replace(search: getcwd(), replace: '', subject: $path) . ''
);
return ConsoleCommand::SUCCESS;
diff --git a/Console/Commands/RedoCommand.php b/src/Console/Commands/MigrateRedoCommand.php
similarity index 97%
rename from Console/Commands/RedoCommand.php
rename to src/Console/Commands/MigrateRedoCommand.php
index 6ac7fa9..dc4353e 100644
--- a/Console/Commands/RedoCommand.php
+++ b/src/Console/Commands/MigrateRedoCommand.php
@@ -8,7 +8,7 @@
use Qubus\Exception\Exception;
use Symfony\Component\Console\Input\InputArgument;
-class RedoCommand extends PhpMigCommand
+class MigrateRedoCommand extends PhpMigCommand
{
protected string $name = 'migrate:redo';
diff --git a/Console/Commands/RollbackCommand.php b/src/Console/Commands/MigrateRollbackCommand.php
similarity index 98%
rename from Console/Commands/RollbackCommand.php
rename to src/Console/Commands/MigrateRollbackCommand.php
index 098755a..ff1d13e 100644
--- a/Console/Commands/RollbackCommand.php
+++ b/src/Console/Commands/MigrateRollbackCommand.php
@@ -8,7 +8,7 @@
use Qubus\Exception\Exception;
use Symfony\Component\Console\Input\InputArgument;
-class RollbackCommand extends PhpMigCommand
+class MigrateRollbackCommand extends PhpMigCommand
{
protected string $name = 'migrate:rollback';
diff --git a/Console/Commands/StatusCommand.php b/src/Console/Commands/MigrateStatusCommand.php
similarity index 97%
rename from Console/Commands/StatusCommand.php
rename to src/Console/Commands/MigrateStatusCommand.php
index d8f77ff..e6f8fab 100644
--- a/Console/Commands/StatusCommand.php
+++ b/src/Console/Commands/MigrateStatusCommand.php
@@ -10,7 +10,7 @@
use function sprintf;
-class StatusCommand extends PhpMigCommand
+class MigrateStatusCommand extends PhpMigCommand
{
protected string $name = 'migrate:status';
diff --git a/Console/Commands/UpCommand.php b/src/Console/Commands/MigrateUpCommand.php
similarity index 97%
rename from Console/Commands/UpCommand.php
rename to src/Console/Commands/MigrateUpCommand.php
index 062b889..d497fbe 100644
--- a/Console/Commands/UpCommand.php
+++ b/src/Console/Commands/MigrateUpCommand.php
@@ -8,7 +8,7 @@
use Qubus\Exception\Exception;
use Symfony\Component\Console\Input\InputArgument;
-class UpCommand extends PhpMigCommand
+class MigrateUpCommand extends PhpMigCommand
{
protected string $name = 'migrate:up';
diff --git a/Console/Commands/PasswordHashCommand.php b/src/Console/Commands/PasswordHashCommand.php
similarity index 93%
rename from Console/Commands/PasswordHashCommand.php
rename to src/Console/Commands/PasswordHashCommand.php
index c051902..a834db1 100644
--- a/Console/Commands/PasswordHashCommand.php
+++ b/src/Console/Commands/PasswordHashCommand.php
@@ -7,6 +7,7 @@
use Codefy\Framework\Application;
use Codefy\Framework\Console\ConsoleCommand;
use Codefy\Framework\Support\Password;
+use Qubus\Exception\Exception;
class PasswordHashCommand extends ConsoleCommand
{
@@ -23,6 +24,9 @@ public function __construct(protected Application $codefy)
parent::__construct(codefy: $codefy);
}
+ /**
+ * @throws Exception
+ */
public function handle(): int
{
$password = $this->getArgument(key: 'password');
diff --git a/Console/Commands/PhpMigCommand.php b/src/Console/Commands/PhpMigCommand.php
similarity index 96%
rename from Console/Commands/PhpMigCommand.php
rename to src/Console/Commands/PhpMigCommand.php
index 6f77ad1..b9e65d9 100644
--- a/Console/Commands/PhpMigCommand.php
+++ b/src/Console/Commands/PhpMigCommand.php
@@ -7,11 +7,11 @@
use ArrayAccess;
use Codefy\Framework\Application;
use Codefy\Framework\Console\ConsoleCommand;
-use Codefy\Framework\Migration\Adapter\MigrationAdapter;
-use Codefy\Framework\Migration\Migration;
-use Codefy\Framework\Migration\Migrator;
use Qubus\Exception\Data\TypeException;
use Qubus\Exception\Exception;
+use Qubus\Expressive\Migration\Adapter\MigrationAdapter;
+use Qubus\Expressive\Migration\Migration;
+use Qubus\Expressive\Migration\Migrator;
use RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -135,7 +135,7 @@ protected function bootstrapAdapter(InputInterface $input): MigrationAdapter
if (!($adapter instanceof MigrationAdapter)) {
throw new RuntimeException(
message: "phpmig.adapter or phpmig.sets must be an
- instance of \\Codefy\\Framework\\Migration\\Adapter\\MigrationAdapter"
+ instance of \Qubus\Expressive\Migration\Adapter\MigrationAdapter"
);
}
@@ -226,7 +226,7 @@ protected function bootstrapMigrations(InputInterface $input, OutputInterface $o
}
$migrationName = preg_replace(pattern: '/^[0-9]+_/', replacement: '', subject: basename(path: $path));
- if (false !== strpos(haystack: $migrationName, needle: '.')) {
+ if (str_contains($migrationName, '.')) {
$migrationName = substr(
string: $migrationName,
offset: 0,
@@ -237,7 +237,7 @@ protected function bootstrapMigrations(InputInterface $input, OutputInterface $o
$class = $this->migrationToClassName(migrationName: $migrationName);
if (
- $this instanceof GenerateCommand
+ $this instanceof MigrateGenerateCommand
&& $class == $this->migrationToClassName(migrationName: $input->getArgument(name: 'name'))
) {
throw new TypeException(
@@ -276,7 +276,7 @@ protected function bootstrapMigrations(InputInterface $input, OutputInterface $o
if (!($migration instanceof Migration)) {
throw new TypeException(
message: sprintf(
- 'The class "%s" in file "%s" must extend \Codefy\Framework\Migration\Migration',
+ 'The class "%s" in file "%s" must extend \Qubus\Expressive\Migration\Migration',
$class,
$path
)
@@ -299,9 +299,9 @@ protected function bootstrapMigrations(InputInterface $input, OutputInterface $o
/**
* @param OutputInterface $output
- * @return mixed
+ * @return Migrator
*/
- protected function bootstrapMigrator(OutputInterface $output): mixed
+ protected function bootstrapMigrator(OutputInterface $output): Migrator
{
return new Migrator(adapter: $this->getAdapter(), objectmap: $this->getObjectMap(), output: $output);
}
diff --git a/src/Console/Commands/QueueListCommand.php b/src/Console/Commands/QueueListCommand.php
new file mode 100644
index 0000000..acaf614
--- /dev/null
+++ b/src/Console/Commands/QueueListCommand.php
@@ -0,0 +1,83 @@
+queue:list command prints a table of available queues.
+php codex queue:list
+EOT;
+
+ protected function configure(): void
+ {
+ parent::configure();
+
+ $this
+ ->addOption(
+ name: 'name',
+ shortcut: null,
+ mode: InputOption::VALUE_REQUIRED,
+ description: 'List the queued jobs by node.'
+ );
+ }
+
+ public function handle(): int
+ {
+ try {
+ $table = new Table($this->output);
+ $table->setHeaders([
+ "Queue Name",
+ "Class",
+ "Expression",
+ "Next Execution",
+ ]);
+
+ $path = "" !== $this->getOptions('name') ? $this->getOptions('name') : 'nodequeue';
+
+ $db = Node::open(database_path(path: $path));
+ $queues = $db->all();
+
+ foreach ($queues as $queue) {
+ $object = new JsonSerializer()->unserialize($queue['object']);
+
+ $nextRun = new CronExpression($object->schedule);
+
+ $table->addRow([
+ $queue['name'],
+ get_class($object),
+ $nextRun->getExpression(),
+ $nextRun->getNextRunDate()->format(format: 'd F Y h:i A'),
+ ]);
+ }
+
+ $table->render();
+ } catch (InvalidJsonException | ReflectionException | Exception $e) {
+ return ConsoleCommand::FAILURE;
+ }
+
+ // return value is important when using CI
+ // to fail the build when the command fails
+ // 0 = success, other values = fail
+ return ConsoleCommand::SUCCESS;
+ }
+}
diff --git a/src/Console/Commands/QueueRunCommand.php b/src/Console/Commands/QueueRunCommand.php
new file mode 100644
index 0000000..e12d77f
--- /dev/null
+++ b/src/Console/Commands/QueueRunCommand.php
@@ -0,0 +1,63 @@
+queue:run command dispatches the queues that are due to run.
+php codex queue:run
+EOT;
+
+ protected function configure(): void
+ {
+ parent::configure();
+
+ $this
+ ->addOption(
+ name: 'name',
+ shortcut: null,
+ mode: InputOption::VALUE_REQUIRED,
+ description: 'Runs the queue based on given node.'
+ );
+ }
+
+ public function handle(): int
+ {
+ try {
+ $path = "" !== $this->getOptions('name') ? $this->getOptions('name') : 'nodequeue';
+
+ $db = Node::open(database_path(path: $path));
+ $queues = $db->all();
+
+ foreach ($queues as $queue) {
+ $object = new JsonSerializer()->unserialize($queue['object']);
+ queue($object)->dispatch();
+ }
+ } catch (InvalidJsonException | ReflectionException | TypeException $e) {
+ return ConsoleCommand::FAILURE;
+ }
+
+ // return value is important when using CI
+ // to fail the build when the command fails
+ // 0 = success, other values = fail
+ return ConsoleCommand::SUCCESS;
+ }
+}
diff --git a/Console/Commands/ListCommand.php b/src/Console/Commands/ScheduleListCommand.php
similarity index 87%
rename from Console/Commands/ListCommand.php
rename to src/Console/Commands/ScheduleListCommand.php
index b8ec3e0..6575e76 100644
--- a/Console/Commands/ListCommand.php
+++ b/src/Console/Commands/ScheduleListCommand.php
@@ -12,13 +12,16 @@
use Exception;
use Symfony\Component\Console\Helper\Table;
-class ListCommand extends ConsoleCommand
+class ScheduleListCommand extends ConsoleCommand
{
protected string $name = 'schedule:list';
protected string $description = 'Lists the existing jobs/tasks.';
- protected string $help = 'This command displays the list of registered jobs/tasks.';
+ protected string $help = <<schedule:list command prints a table with jobs/tasks to run.
+php codex schedule:list
+EOT;
public function __construct(protected Schedule $schedule, protected Application $codefy)
{
@@ -45,7 +48,7 @@ public function handle(): int
$nextRun = new CronExpression($job->getExpression());
$fullCommand = $job->getCommand();
- if($job instanceof Callback) {
+ if ($job instanceof Callback) {
$fullCommand = $job->__toString();
}
diff --git a/Console/Commands/ScheduleRunCommand.php b/src/Console/Commands/ScheduleRunCommand.php
similarity index 100%
rename from Console/Commands/ScheduleRunCommand.php
rename to src/Console/Commands/ScheduleRunCommand.php
diff --git a/Console/Commands/ServeCommand.php b/src/Console/Commands/ServeCommand.php
similarity index 100%
rename from Console/Commands/ServeCommand.php
rename to src/Console/Commands/ServeCommand.php
diff --git a/Console/Commands/Traits/MakeCommandAware.php b/src/Console/Commands/Traits/MakeCommandAware.php
similarity index 98%
rename from Console/Commands/Traits/MakeCommandAware.php
rename to src/Console/Commands/Traits/MakeCommandAware.php
index 60792ca..57c2bc7 100644
--- a/Console/Commands/Traits/MakeCommandAware.php
+++ b/src/Console/Commands/Traits/MakeCommandAware.php
@@ -12,6 +12,7 @@
use League\Flysystem\FilesystemException;
use Qubus\Exception\Data\TypeException;
use Qubus\Support\Inflector;
+use ReflectionException;
use RuntimeException;
use function str_contains;
@@ -103,6 +104,8 @@ classNameSuffix: $classNameSuffix,
* @param string|null $qualifiedNamespaces - will return the namespace for the stub command
* @return void
* @throws MakeCommandFileAlreadyExistsException
+ * @throws \Qubus\Exception\Exception
+ * @throws ReflectionException
*/
public function createClassFromStub(
string $qualifiedClass,
@@ -173,7 +176,7 @@ private function addOptionalDirFlag(mixed $options): string
*/
private function getStubFiles(string $classNameSuffix): string|false
{
- $files = glob(pattern: Application::$ROOT_PATH . '/vendor/codefyphp/framework/Stubs/*.stub');
+ $files = glob(pattern: Application::$ROOT_PATH . '/vendor/codefyphp/codefy/src/Stubs/*.stub');
if (is_array(value: $files) && count($files)) {
foreach ($files as $file) {
if (is_file(filename: $file)) {
diff --git a/Console/Commands/UlidCommand.php b/src/Console/Commands/UlidCommand.php
similarity index 100%
rename from Console/Commands/UlidCommand.php
rename to src/Console/Commands/UlidCommand.php
diff --git a/Console/Commands/UuidCommand.php b/src/Console/Commands/UuidCommand.php
similarity index 100%
rename from Console/Commands/UuidCommand.php
rename to src/Console/Commands/UuidCommand.php
diff --git a/src/Console/Commands/VendorPublishCommand.php b/src/Console/Commands/VendorPublishCommand.php
new file mode 100644
index 0000000..0139ad9
--- /dev/null
+++ b/src/Console/Commands/VendorPublishCommand.php
@@ -0,0 +1,266 @@
+addOption(
+ name: 'provider',
+ shortcut: null,
+ mode: InputOption::VALUE_REQUIRED,
+ description: 'ServiceProvider class'
+ )
+ ->addOption(
+ name: 'tag',
+ shortcut: null,
+ mode: InputOption::VALUE_REQUIRED,
+ description: 'Publish group (config, migrations, etc.).'
+ )
+ ->addOption(
+ name: 'force',
+ shortcut: 'f',
+ mode: InputOption::VALUE_NONE,
+ description: 'Overwrite existing files.'
+ )
+ ->addOption(
+ name: 'list',
+ shortcut: 'l',
+ mode: InputOption::VALUE_NONE,
+ description: 'List publishable providers and tags.'
+ )
+ ->setDescription(description: 'Publish any publishable assets from vendor packages')
+ ->setHelp(
+ help: <<vendor:publish publishes assets from vendor packages.
+php codex vendor:publish
+EOT
+ );
+ }
+
+ /**
+ * @throws FilesystemException
+ */
+ public function handle(): int
+ {
+ // If user wants to list publishable tags instead of publishing
+ if ($this->input->getOption('list')) {
+ return $this->listPublishables();
+ }
+
+ // If user wants to publish from a specific provider
+ if ($this->input->getOption('provider')) {
+ return $this->publishDefinedProvider();
+ }
+
+ $tag = $this->input->getOption('tag');
+ $force = $this->input->getOption('force');
+
+ $registeredProviders = $this->codefy->getRegisteredProviders();
+
+ foreach ($registeredProviders as $instance => $value) {
+
+ $paths = $instance->pathsToPublish($tag);
+
+ foreach ($paths as $from => $type) {
+ $destination = $this->resolveDestination($from, $type);
+
+ if (is_dir($from)) {
+ $this->copyDirectory($from, $destination, $force);
+ } else {
+ $this->copyFile($from, $destination, $force);
+ }
+ }
+ }
+
+ return ConsoleCommand::SUCCESS;
+ }
+
+ private function resolveDestination(string $from, string $type): string
+ {
+ // Normalize trailing separators
+ $from = rtrim($from, $this->codefy::DS);
+
+ return match ($type) {
+ 'config' => is_dir($from) ? 'config' : 'config/' . basename($from),
+ 'migrations' => is_dir($from) ? 'database/migrations' : 'database/migrations/' . basename($from),
+ default => $type, // allow explicit path fallback
+ };
+ }
+
+ /**
+ * @throws FilesystemException
+ */
+ private function copyFile(string $from, string $to, bool $force): void
+ {
+ /** @var FileSystem $filesystem */
+ $filesystem = $this->codefy->make(name: 'filesystem.default');
+
+ // Ensure parent directory exists in Flysystem
+ $dir = pathinfo($to, PATHINFO_DIRNAME);
+ if ($dir !== '' && ! $filesystem->directoryExists($dir)) {
+ $filesystem->createDirectory($dir);
+ }
+
+ if (! $force && $filesystem->fileExists($to)) {
+ $this->output->writeln("Skipped {$to}, file exists.");
+ return;
+ }
+
+ // Prefer streaming for large files, fallback to write()
+ $stream = fopen($from, 'rb');
+ if ($stream !== false && method_exists($filesystem, 'writeStream')) {
+ $filesystem->writeStream($to, $stream);
+ if (is_resource($stream)) {
+ fclose($stream);
+ }
+ } else {
+ $contents = file_get_contents($from);
+ $filesystem->write($to, $contents === false ? '' : $contents);
+ }
+
+ $this->output->writeln("Published: {$from} -> {$to}");
+ }
+
+ /**
+ * @throws FilesystemException
+ */
+ private function copyDirectory(string $from, string $to, bool $force): void
+ {
+ $from = rtrim($from, $this->codefy::DS);
+ $to = rtrim($to, $this->codefy::DS);
+
+ $dirIterator = new RecursiveDirectoryIterator($from, FilesystemIterator::SKIP_DOTS);
+ $iterator = new RecursiveIteratorIterator($dirIterator, RecursiveIteratorIterator::LEAVES_ONLY);
+
+ foreach ($iterator as $fileInfo) {
+ /** @var SplFileInfo $fileInfo */
+ if ($fileInfo->isDir()) {
+ continue;
+ }
+
+ // Compute relative path robustly (works on Windows and *nix)
+ $absolutePath = $fileInfo->getPathname();
+ $relative = substr($absolutePath, strlen($from) + 1);
+ $relative = str_replace(['\\', '/'], $this->codefy::DS, $relative);
+
+ $target = $to . $this->codefy::DS . $relative;
+
+ $this->copyFile($absolutePath, $target, $force);
+ }
+ }
+
+ private function listPublishables(): int
+ {
+ $providers = $this->codefy->getRegisteredProviders();
+
+ $this->output->writeln("Available publishable resources:");
+
+ foreach ($providers as $instance => $value) {
+
+ foreach (['config', 'migrations'] as $tag) {
+ $paths = $instance->pathsToPublish($tag);
+ if (!empty($paths)) {
+ $this->output->writeln(" - tag: {$tag}");
+ foreach ($paths as $from => $type) {
+ $this->output->writeln(" {$from} -> {$this->resolveDestination($from, $type)}");
+ }
+ }
+ }
+ }
+
+ return ConsoleCommand::SUCCESS;
+ }
+
+ /**
+ * @throws FilesystemException
+ */
+ private function publishDefinedProvider(): int
+ {
+ $registeredProviders = $this->codefy->getRegisteredProviders();
+
+ $provider = $this->input->getOption('provider');
+ $tag = $this->input->getOption('tag');
+ $force = $this->input->getOption('force');
+
+ $providers = $this->findKeysLike($provider, $registeredProviders);
+
+ foreach ($providers as $int => $p) {
+
+ $paths = $p->pathsToPublish($tag);
+
+ foreach ($paths as $from => $type) {
+ $destination = $this->resolveDestination($from, $type);
+
+ if (is_dir($from)) {
+ $this->copyDirectory($from, $destination, $force);
+ } else {
+ $this->copyFile($from, $destination, $force);
+ }
+ }
+ }
+
+ return ConsoleCommand::SUCCESS;
+ }
+
+ /**
+ * Search array keys for a substring and return all matching keys.
+ *
+ * @param string $needle
+ * @param array $haystack
+ * @param bool $caseSensitive
+ *
+ * @return array List of matching keys (empty if none found).
+ */
+ protected function findKeysLike(string $needle, array $haystack, bool $caseSensitive = true): array
+ {
+ $matches = [];
+
+ foreach ($haystack as $key => $_) {
+ if (!is_string($key)) {
+ continue;
+ }
+
+ $hay = $caseSensitive ? $key : strtolower($key);
+ $needleCmp = $caseSensitive ? $needle : strtolower($needle);
+
+ if (str_contains($hay, $needleCmp)) {
+ $matches[] = $key;
+ }
+ }
+
+ return $matches;
+ }
+}
diff --git a/Console/ConsoleApplication.php b/src/Console/ConsoleApplication.php
similarity index 87%
rename from Console/ConsoleApplication.php
rename to src/Console/ConsoleApplication.php
index 864730e..1867a14 100644
--- a/Console/ConsoleApplication.php
+++ b/src/Console/ConsoleApplication.php
@@ -14,6 +14,7 @@
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;
+use Throwable;
class ConsoleApplication extends SymfonyApplication
{
@@ -24,7 +25,10 @@ public function __construct(protected Application $codefy)
parent::__construct(name: 'CodefyPHP', version: Application::APP_VERSION);
}
- public function run(InputInterface $input = null, OutputInterface $output = null): int
+ /**
+ * @throws Throwable
+ */
+ public function run(?InputInterface $input = null, ?OutputInterface $output = null): int
{
$this->getCommandName(input: $input = $input ?: new ArgvInput());
@@ -33,8 +37,9 @@ public function run(InputInterface $input = null, OutputInterface $output = null
/**
* @throws Exception
+ * @throws Throwable
*/
- public function call($command, array $parameters = [], bool|OutputInterface $outputBuffer = null): int
+ public function call($command, array $parameters = [], bool|OutputInterface|null $outputBuffer = null): int
{
[$command, $input] = $this->parseCommand(command: $command, parameters: $parameters);
@@ -75,7 +80,7 @@ protected function parseCommand(string $command, array $parameters): array
*/
public function output(): string
{
- return $this->lastOutput && method_exists($this->lastOutput, 'fetch')
+ return $this->lastOutput && method_exists(object_or_class: $this->lastOutput, method: 'fetch')
? $this->lastOutput->fetch()
: '';
}
diff --git a/Console/ConsoleCommand.php b/src/Console/ConsoleCommand.php
similarity index 63%
rename from Console/ConsoleCommand.php
rename to src/Console/ConsoleCommand.php
index 0dcc26f..f3e661d 100644
--- a/Console/ConsoleCommand.php
+++ b/src/Console/ConsoleCommand.php
@@ -9,13 +9,20 @@
use ReflectionException;
use ReflectionMethod;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
+use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ChoiceQuestion;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+use Symfony\Component\Console\Question\Question;
use function count;
use function method_exists;
+use function str_repeat;
+
+use const PHP_EOL;
abstract class ConsoleCommand extends SymfonyCommand
{
@@ -102,55 +109,60 @@ protected function getOptions(?string $key = null): mixed
* Outputs the string to the console without any tag.
*
* @param string $string
- * @return mixed
*/
- protected function terminalRaw(string $string): mixed
+ protected function terminalRaw(string $string): void
{
- return $this->output->writeln(messages: $string);
+ $this->output->writeln(messages: $string);
}
/**
* Output to the terminal wrap in info tags.
*
* @param string $string
- * @return string
*/
- protected function terminalInfo(string $string): mixed
+ protected function terminalInfo(string $string): void
{
- return $this->output->writeln(messages: '' . $string . '');
+ $this->output->writeln(messages: '' . $string . '');
}
/**
* Output to the terminal wrap in comment tags.
*
* @param string $string
- * @return string
*/
- protected function terminalComment(string $string): mixed
+ protected function terminalComment(string $string): void
{
- return $this->output->writeln(messages: '' . $string . '');
+ $this->output->writeln(messages: '' . $string . '');
}
/**
* Output to the terminal wrap in question tags.
*
* @param string $string
- * @return string
*/
- protected function terminalQuestion(string $string): mixed
+ protected function terminalQuestion(string $string): void
{
- return $this->output->writeln(messages: '' . $string . '');
+ $this->output->writeln(messages: '' . $string . '');
}
/**
* Output to the terminal wrap in error tags.
*
* @param string $string
- * @return string
*/
- protected function terminalError(string $string): mixed
+ protected function terminalError(string $string): void
{
- return $this->output->writeln(messages: '' . $string . '');
+ $this->output->writeln(messages: '' . $string . '');
+ }
+
+ /**
+ * Output to the terminal with a blank line.
+ *
+ * @param int $count
+ */
+ protected function terminalNewLine(int $count = 1): void
+ {
+ $this->output->write(str_repeat(PHP_EOL, $count));
}
/**
@@ -177,6 +189,78 @@ private function setArguments(): ConsoleCommand|bool
return true;
}
+ /**
+ * @param string $question
+ * @return mixed
+ */
+ protected function confirm(string $question): mixed
+ {
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper(name: 'question');
+ $question = new ConfirmationQuestion(question: $question, default: false);
+
+ return $helper->ask($this->input, $this->output, $question);
+ }
+
+ /**
+ * @param string $question
+ * @param bool|float|int|string|null $default
+ * @return mixed
+ */
+ protected function ask(string $question, bool|float|int|null|string $default = null): mixed
+ {
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper(name: 'question');
+ $question = new Question(question: $question, default: $default);
+
+ return $helper->ask($this->input, $this->output, $question);
+ }
+
+ /**
+ * @param string $question
+ * @param array $choices
+ * @param bool|float|int|string|null $default
+ * @param string|null $message
+ * @return mixed
+ */
+ protected function choice(
+ string $question,
+ array $choices,
+ bool|float|int|null|string $default = null,
+ ?string $message = null
+ ): mixed {
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper(name: 'question');
+ $question = new ChoiceQuestion(question: $question, choices: $choices, default: $default);
+
+ $question->setErrorMessage(errorMessage: $message ?? 'There is an error.');
+
+ return $helper->ask($this->input, $this->output, $question);
+ }
+
+ /**
+ * @param string $question
+ * @param array $choices
+ * @param bool|float|int|string|null $default
+ * @param string|null $message
+ * @return mixed
+ */
+ protected function multiChoice(
+ string $question,
+ array $choices,
+ bool|float|int|null|string $default = null,
+ ?string $message = null
+ ): mixed {
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper(name: 'question');
+ $question = new ChoiceQuestion(question: $question, choices: $choices, default: $default);
+
+ $question->setMultiselect(multiselect: true);
+ $question->setErrorMessage(errorMessage: $message ?? 'There is an error.');
+
+ return $helper->ask($this->input, $this->output, $question);
+ }
+
/**
* @return bool|ConsoleCommand
* @throws TypeException
diff --git a/Console/ConsoleKernel.php b/src/Console/ConsoleKernel.php
similarity index 72%
rename from Console/ConsoleKernel.php
rename to src/Console/ConsoleKernel.php
index 57b1c7a..eca4301 100644
--- a/Console/ConsoleKernel.php
+++ b/src/Console/ConsoleKernel.php
@@ -6,10 +6,13 @@
use Codefy\Framework\Application;
use Codefy\Framework\Console\ConsoleApplication as Codex;
+use Codefy\Framework\Contracts\Console\Kernel;
use Codefy\Framework\Factory\FileLoggerSmtpFactory;
use Codefy\Framework\Scheduler\Mutex\Locker;
use Codefy\Framework\Scheduler\Schedule;
+use Codefy\Framework\Support\DefaultCommands;
use Exception;
+use Psr\EventDispatcher\EventDispatcherInterface;
use Qubus\Support\DateTime\QubusDateTimeZone;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;
@@ -17,6 +20,7 @@
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
+use function array_unique;
use function Qubus\Inheritance\Helpers\tap;
class ConsoleKernel implements Kernel
@@ -46,13 +50,19 @@ protected function defineConsoleSchedule(): void
$this->codefy->share(nameOrInstance: Schedule::class);
$this->codefy->prepare(name: Schedule::class, callableOrMethodStr: function () {
+ $dispatcher = $this->codefy->make(name: EventDispatcherInterface::class);
+
$timeZone = new QubusDateTimeZone(timezone: $this->codefy->make(
name: 'codefy.config'
)->getConfigKey('app.timezone'));
$mutex = $this->codefy->make(name: Locker::class);
- return tap(value: new Schedule(timeZone: $timeZone, mutex: $mutex), callback: function ($schedule) {
+ return tap(value: new Schedule(
+ dispatcher: $dispatcher,
+ timeZone: $timeZone,
+ mutex: $mutex
+ ), callback: function ($schedule) {
$this->schedule(schedule: $schedule);
});
});
@@ -89,14 +99,32 @@ protected function commands(): void
/**
* Registers a command.
*
- * @param Command $command
+ * @param callable|Command $command
* @return void
*/
- public function registerCommand(Command $command): void
+ public function registerCommand(callable|Command $command): void
{
$this->getCodex()->add(command: $command);
}
+ /**
+ * Add an array of commands to the console.
+ *
+ * @param array $commands
+ * @return void
+ */
+ public function addCommands(array $commands): void
+ {
+ foreach ($commands as $command) {
+ if (is_callable($command)) {
+ $command = $command($this->codefy);
+ } else {
+ $command = $this->codefy->make(name: $command);
+ }
+ $this->registerCommand($command);
+ }
+ }
+
/**
* {@inheritDoc}
*/
@@ -147,13 +175,30 @@ protected function getCodex(): Codex
return $this->codex;
}
+ /**
+ * @param array $commands
+ * @return void
+ */
+ protected function load(array $commands = []): void
+ {
+ $commands = array_unique(
+ array_merge(
+ $this->commands,
+ $this->codefy->make(name: 'codefy.config')->getConfigKey('app.commands'),
+ new DefaultCommands($commands)->toArray()
+ )
+ );
+
+ $this->addCommands($commands);
+ }
+
/**
* {@inheritDoc}
*
* @throws CommandNotFoundException
* @throws Exception
*/
- public function call(string $command, array $parameters = [], bool|OutputInterface $outputBuffer = null): int
+ public function call(string $command, array $parameters = [], bool|OutputInterface|null $outputBuffer = null): int
{
$this->bootstrap();
diff --git a/Console/Exceptions/MakeCommandFileAlreadyExistsException.php b/src/Console/Exceptions/MakeCommandFileAlreadyExistsException.php
similarity index 100%
rename from Console/Exceptions/MakeCommandFileAlreadyExistsException.php
rename to src/Console/Exceptions/MakeCommandFileAlreadyExistsException.php
diff --git a/Console/Kernel.php b/src/Contracts/Console/Kernel.php
similarity index 91%
rename from Console/Kernel.php
rename to src/Contracts/Console/Kernel.php
index 2abad8d..6f9a7aa 100644
--- a/Console/Kernel.php
+++ b/src/Contracts/Console/Kernel.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Codefy\Framework\Console;
+namespace Codefy\Framework\Contracts\Console;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -47,5 +47,5 @@ public function bootstrap(): void;
* @param bool|OutputInterface|null $outputBuffer
* @return int
*/
- public function call(string $command, array $parameters = [], bool|OutputInterface $outputBuffer = null): int;
+ public function call(string $command, array $parameters = [], bool|OutputInterface|null $outputBuffer = null): int;
}
diff --git a/src/Contracts/Http/Kernel.php b/src/Contracts/Http/Kernel.php
new file mode 100644
index 0000000..f156c45
--- /dev/null
+++ b/src/Contracts/Http/Kernel.php
@@ -0,0 +1,33 @@
+getContainer();
+ }
+ return $app->getContainer()->make($name, $args);
+}
+
+/**
+ * Get the available config instance.
+ *
+ * @param array|string|null $key
+ * @param mixed $default
+ * @return mixed
+ */
+function config(string|array|null $key, mixed $default = ''): mixed
+{
+ if (is_null__($key)) {
+ return app(name: 'codefy.config');
+ }
+
+ if (is_array($key)) {
+ app(name: 'codefy.config')->setConfigKey($key[0], $key[1]);
+ }
+
+ return app(name: 'codefy.config')->getConfigKey($key, $default);
+}
+
+/**
+ * Retrieve a fresh instance of the bootstrap.
+ *
+ * @return mixed
+ */
+function get_fresh_bootstrap(): mixed
+{
+ if (file_exists(filename: $file = getcwd() . '/bootstrap/app.php')) {
+ return require(realpath(path: $file));
+ } elseif (file_exists(filename: $file = dirname(path: getcwd()) . '/bootstrap/app.php')) {
+ return require(realpath(path: $file));
+ } else {
+ return require(realpath(path: dirname(path: getcwd()) . '/bootstrap/app.php'));
+ }
+}
+
+/**
+ * Gets the value of an environment variable.
+ *
+ * @param string $key
+ * @param mixed|null $default
+ * @return mixed
+ */
+function env(string $key, mixed $default = null): mixed
+{
+ return \Qubus\Config\Helpers\env($key, $default);
+}
+
+/**
+ * Database abstraction layer global function.
+ *
+ * @throws Exception
+ */
+function dbal(): Connection
+{
+ return Codefy::$PHP->getDbConnection();
+}
+
+/**
+ * QueryBuilder global function.
+ *
+ * @return QueryBuilder|null
+ * @throws Exception
+ */
+function queryBuilder(): ?QueryBuilder
+{
+ return dbal()->queryBuilder();
+}
+
+/**
+ * Alternative to PHP's native mail function with SMTP support.
+ *
+ * This is a simple mail function to see for testing or for
+ * sending simple email messages.
+ *
+ * @param string|array $to Recipient(s)
+ * @param string $subject Subject of the email.
+ * @param string $message The email body.
+ * @param array $headers An array of headers.
+ * @param array $attachments An array of attachments.
+ * @return bool
+ * @throws Exception|ReflectionException|\PHPMailer\PHPMailer\Exception
+ */
+function mail(string|array $to, string $subject, string $message, array $headers = [], array $attachments = []): bool
+{
+ // Instantiate CodefyMailer.
+ $instance = new CodefyMailer(config: app(name: 'codefy.config'));
+
+ // Set the mailer transport.
+ $func = sprintf('with%s', ucfirst(config(key: 'mailer.mail_transport')));
+ $instance = $instance->{$func}();
+
+ // Detect HTML markdown.
+ if (substr_count(haystack: $message, needle: '') >= 1) {
+ $instance = $instance->withHtml(isHtml: true);
+ }
+
+ // Build recipient(s).
+ $instance = $instance->withTo(address: $to);
+
+ // Set from name and from email from environment variables.
+ $fromName = __observer()->filter->applyFilter('mail.from.name', env(key: 'MAILER_FROM_NAME'));
+ $fromEmail = __observer()->filter->applyFilter('mail.from.email', env(key: 'MAILER_FROM_EMAIL'));
+ // Set charset
+ $charset = __observer()->filter->applyFilter('mail.charset', 'utf-8');
+
+ // Set email subject and body.
+ $instance = $instance->withSubject(subject: $subject)->withBody(data: $message);
+
+ // Check for other headers and loop through them.
+ if (!empty($headers)) {
+ foreach ($headers as $name => $content) {
+ if ($name === 'cc') {
+ $instance = $instance->withCc(address: $content);
+ }
+
+ if ($name === 'bcc') {
+ $instance = $instance->withBcc(address: $content);
+ }
+
+ if ($name === 'replyTo') {
+ $instance = $instance->withReplyTo(address: $content);
+ }
+
+ if (
+ ! in_array(needle: $name, haystack: ['MIME-Version','to','cc','bcc','replyTo'], strict: true)
+ && !is_int($name)
+ ) {
+ $instance = $instance->withCustomHeader(name: (string) $name, value: $content);
+ }
+ }
+ }
+
+ // Set X-Mailer header
+ $xMailer = __observer()->filter->applyFilter(
+ 'mail.xmailer',
+ sprintf('CodefyPHP Framework %s', Codefy::$PHP::APP_VERSION)
+ );
+ $instance = $instance->withXMailer(xmailer: $xMailer);
+
+ // Set email charset
+ $instance = $instance->withCharset(charset: $charset ?: 'utf-8');
+
+ // Check if there are attachments and loop through them.
+ if (!empty($attachments)) {
+ foreach ($attachments as $filename => $filepath) {
+ $filename = is_string(value: $filename) ? $filename : '';
+ $instance = $instance->withAttachment(path: $filepath, name: $filename);
+ }
+ }
+
+ // Set sender.
+ $instance = $instance->withFrom(address: $fromEmail, name: $fromName ?: '');
+
+ try {
+ return $instance->send();
+ } catch (\PHPMailer\PHPMailer\Exception $e) {
+ FileLoggerFactory::getLogger()->error($e->getMessage(), ['function' => '\Codefy\Framework\Helpers\mail']);
+ return false;
+ }
+}
+
+/**
+ * Dispatches the given `$command` through
+ * the CommandBus.
+ *
+ * @param Command $command
+ * @throws ReflectionException
+ * @throws CommandCouldNotBeHandledException
+ * @throws UnresolvableCommandHandlerException
+ */
+function command(Command $command): void
+{
+ $resolver = new NativeCommandHandlerResolver(
+ container: ContainerFactory::make(config: config(key: 'commandbus.container'))
+ );
+ $odin = new Odin(bus: new SynchronousCommandBus($resolver));
+
+ $odin->execute($command);
+}
+
+/**
+ * Queries the given query and returns
+ * a result if any.
+ *
+ * @throws ReflectionException
+ * @throws UnresolvableQueryHandlerException
+ */
+function ask(Query $query): mixed
+{
+ $resolver = new NativeQueryHandlerResolver(
+ container: ContainerFactory::make(config: config(key: 'querybus.aliases'))
+ );
+ $enquirer = new Enquire(bus: new SynchronousQueryBus($resolver));
+
+ return $enquirer->execute($query);
+}
+
+/**
+ * Normalize a URL by collapsing multiple consecutive slashes into one,
+ * but preserve the scheme's "://" (for http/https) and do not touch query or fragment.
+ *
+ * Examples:
+ * normalize_url('http://example.com//foo///bar') => 'http://example.com/foo/bar'
+ * normalize_url('//example.com//a') => '//example.com/a'
+ * normalize_url('/some//relative//path') => '/some/relative/path'
+ *
+ * @param string $url
+ * @return string
+ */
+function normalize_url(string $url): string
+{
+ $original = $url;
+ $parts = parse_url($url);
+
+ // If parse_url fails, fall back to simple regex while protecting scheme.
+ if ($parts === false) {
+ if (preg_match('#^(https?://)#i', $url, $m)) {
+ $prefix = $m[1];
+ $rest = substr($url, strlen($prefix));
+ $rest = preg_replace(pattern: '#/+#', replacement: '/', subject: $rest);
+ return $prefix . $rest;
+ }
+
+ if (str_starts_with($url, '//')) {
+ return '//' . preg_replace(pattern: '#/+#', replacement: '/', subject: substr(string: $url, offset: 2));
+ }
+
+ return preg_replace(pattern: '#/+#', replacement: '/', subject: $url);
+ }
+
+ $scheme = $parts['scheme'] ?? null;
+ $user = $parts['user'] ?? null;
+ $pass = $parts['pass'] ?? null;
+ $host = $parts['host'] ?? null;
+ $port = $parts['port'] ?? null;
+ $path = $parts['path'] ?? '';
+ $query = $parts['query'] ?? null;
+ $fragment = $parts['fragment'] ?? null;
+
+ // Collapse multiple slashes in the path only.
+ // This preserves a single leading slash (if any) and turns '///a//b' => '/a/b'.
+ $path = preg_replace(pattern: '#/+#', replacement: '/', subject: $path);
+
+ // Rebuild authority (user[:pass]@host[:port])
+ $authority = '';
+ if ($host !== null) {
+ if ($user !== null) {
+ $authority .= $user;
+ if ($pass !== null) {
+ $authority .= ':' . $pass;
+ }
+ $authority .= '@';
+ }
+
+ // For IPv6 host strings we ensure brackets when reconstructing authority.
+ $hostOut = $host;
+ if (
+ str_contains($hostOut, ':')
+ && $hostOut[0] !== '['
+ && filter_var($hostOut, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
+ ) {
+ $hostOut = '[' . $hostOut . ']';
+ }
+
+ $authority .= $hostOut;
+ if ($port !== null) {
+ $authority .= ':' . $port;
+ }
+ }
+
+ $result = '';
+
+ if ($scheme !== null) {
+ // Preserve scheme and "://"
+ $result .= $scheme . '://';
+ $result .= $authority;
+ } elseif ($host !== null) {
+ // protocol-relative or host-without-scheme: preserve leading //
+ $result .= '//' . $authority;
+ } elseif (str_starts_with($original, '//')) {
+ // preserve protocol-relative leading //
+ $result .= '//';
+ }
+
+ $result .= $path;
+
+ if ($query !== null) {
+ $result .= '?' . $query;
+ }
+
+ if ($fragment !== null) {
+ $result .= '#' . $fragment;
+ }
+
+ return $result;
+}
+
+/**
+ * Displays the returned translated text.
+ *
+ * @param string $string
+ * @return string
+ */
+function trans(string $string): string
+{
+ return t__(msgid: $string, domain: config(key: 'app.locale_domain', default: 'codefy'));
+}
+
+/**
+ * Escapes a translated string to make it safe for HTML output.
+ *
+ * @throws Exception
+ */
+function trans_html(string $string): string
+{
+ return esc_html__(string: $string, domain: config(key: 'app.locale_domain', default: 'codefy'));
+}
+
+/**
+ * Escapes a translated string to make it safe for HTML attribute.
+ *
+ * @throws Exception
+ */
+function trans_attr(string $string): string
+{
+ return esc_attr__(string: $string, domain: config(key: 'app.locale_domain', default: 'codefy'));
+}
+
+/**
+ * Returns the url of the application.
+ */
+function site_url(string $path = ''): string
+{
+ try {
+ return normalize_url(Server::siteUrl($path));
+ } catch (Exception $e) {
+ error_log($e->getMessage());
+ }
+
+ return '';
+}
+
+/**
+ * Queues an item.
+ *
+ * @param ShouldQueue $queue
+ * @return NodeQueue
+ */
+function queue(ShouldQueue $queue): NodeQueue
+{
+ return new NodeQueue($queue);
+}
diff --git a/Helpers/path.php b/src/Helpers/path.php
similarity index 98%
rename from Helpers/path.php
rename to src/Helpers/path.php
index 94accde..fb3b0a8 100644
--- a/Helpers/path.php
+++ b/src/Helpers/path.php
@@ -5,7 +5,6 @@
namespace Codefy\Framework\Helpers;
use Codefy\Framework\Application;
-use Qubus\Exception\Data\TypeException;
use Qubus\Exception\Exception;
use function implode;
diff --git a/Http/BaseController.php b/src/Http/BaseController.php
similarity index 70%
rename from Http/BaseController.php
rename to src/Http/BaseController.php
index 2234e34..1f4f705 100644
--- a/Http/BaseController.php
+++ b/src/Http/BaseController.php
@@ -10,29 +10,21 @@
use Qubus\Http\Session\SessionService;
use Qubus\Routing\Controller\Controller;
use Qubus\Routing\Router;
-use Qubus\View\Native\NativeLoader;
use Qubus\View\Renderer;
-use function Codefy\Framework\Helpers\config;
-
class BaseController extends Controller implements RoutingController
{
public function __construct(
- protected SessionService $sessionService,
- protected Router $router,
- protected ?Renderer $view = null,
+ protected SessionService $sessionService {
+ get => $this->sessionService;
+ },
+ protected Router $router {
+ get => $this->router;
+ },
+ protected Renderer $view {
+ get => $this->view;
+ },
) {
- $this->setView(view: $view ?? new NativeLoader(config('view.path')));
- }
-
- /**
- * Gets the view instance.
- *
- * @return Renderer
- */
- public function getView(): Renderer
- {
- return $this->view;
}
/**
diff --git a/src/Http/Errors/HttpRequestError.php b/src/Http/Errors/HttpRequestError.php
new file mode 100644
index 0000000..c0cf7e5
--- /dev/null
+++ b/src/Http/Errors/HttpRequestError.php
@@ -0,0 +1,15 @@
+ __observer()->filter->applyFilter('http.request.timeout', 10, $uri),
+ /**
+ * Filters the number of seconds to wait while trying to connect to a server.
+ * Use 0 to wait indefinitely. Default: 10.
+ *
+ * @param float $connect_timeout Number of seconds. Default: 10.
+ * @param string|UriInterface $uri URI object or string.
+ */
+ 'connect_timeout' => __observer()->filter->applyFilter('http.request.connect.timeout', 10, $uri),
+ /**
+ * Filters the version of the HTTP protocol used in a request.
+ *
+ * @param string $version HTTP protocol version used (usually '1.1', '1.0' or '2').
+ * Default: 1.1.
+ * @param string|UriInterface $uri URI object or string.
+ */
+ 'version' => __observer()->filter->applyFilter('http.request.version', '1.1', $uri),
+ /**
+ * Filters the redirect behavior of a request.
+ *
+ * @param bool|array $allow_redirects The redirect behavior of a request. Default: false.
+ * @param string|UriInterface $uri URI object or string.
+ */
+ 'allow_redirects ' => __observer()->filter->applyFilter(
+ 'http.request.allow.redirects',
+ false,
+ $uri
+ ),
+ 'headers' => [],
+ 'body' => null,
+ 'delay' => null,
+ 'http_errors' => true,
+ 'proxy' => false,
+ 'stream' => false,
+
+ ];
+
+ // Pre-parse for the HEAD checks.
+ $options = ArgsParser::parse($options);
+ // By default, HEAD requests do not cause redirections.
+ if (isset($options['method']) && $options['method'] === RequestMethod::HEAD) {
+ $defaults['allow_redirects'] = false;
+ }
+
+ $parsedArgs = ArgsParser::parse($options, $defaults);
+ /**
+ * Filters the arguments used in an HTTP request.
+ *
+ * @param array $parsedArgs An array of HTTP request arguments.
+ * @param string|UriInterface $uri URI object or string.
+ */
+ $parsedArgs = __observer()->filter->applyFilter('http.request.args', $parsedArgs, $uri);
+ /**
+ * Filters the preemptive return value of an HTTP request.
+ *
+ * Returning a non-false value from the filter will short-circuit the HTTP request and return
+ * early with that value. A filter should return one of:
+ *
+ * - An array containing 'headers', 'body', and 'response' elements
+ * - A HttpRequestError instance
+ * - bool false to avoid short-circuiting the response
+ *
+ * Returning any other value may result in unexpected behavior.
+ *
+ * @param false|array|HttpRequestError $response A preemptive return value of an HTTP request. Default false.
+ * @param array $parsedArgs HTTP request arguments.
+ * @param string|UriInterface $uri URI object or string.
+ */
+ $preempt = __observer()->filter->applyFilter('http.request.preempt', false, $parsedArgs, $uri);
+ if ($preempt !== false) {
+ return $preempt;
+ }
+
+ if (is_null__($parsedArgs['headers'])) {
+ $parsedArgs['headers'] = [];
+ }
+
+ $response = parent::request($method, $uri, $parsedArgs);
+
+ /**
+ * Fires after an HTTP API response is received and before the response is returned.
+ *
+ * @param ResponseInterface|mixed $response HTTP response.
+ * @param string $context Context under which the hook is fired.
+ * @param string $class HTTP transport used.
+ * @param array $parsedArgs HTTP request arguments.
+ * @param string|UriInterface $uri URI object or string.
+ */
+ __observer()->action->doAction(
+ 'http_api_debug',
+ $response,
+ 'response',
+ \Qubus\Http\Request::class,
+ $parsedArgs,
+ $uri
+ );
+
+ /**
+ * Filters a successful HTTP API response immediately before the response is returned.
+ *
+ * @param ResponseInterface $response HTTP response.
+ * @param array $parsedArgs HTTP request arguments.
+ * @param string|UriInterface $uri URI object or string.
+ */
+ return __observer()->filter->applyFilter('http.request.response', $response, $parsedArgs, $uri);
+ }
+}
diff --git a/Http/Kernel.php b/src/Http/Kernel.php
similarity index 58%
rename from Http/Kernel.php
rename to src/Http/Kernel.php
index b36075d..091a149 100644
--- a/Http/Kernel.php
+++ b/src/Http/Kernel.php
@@ -5,19 +5,23 @@
namespace Codefy\Framework\Http;
use Codefy\Framework\Application;
-use Codefy\Framework\Contracts\Kernel as HttpKernel;
+use Codefy\Framework\Contracts\Http\Kernel as HttpKernel;
use Exception;
-use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
use Qubus\Error\Handlers\DebugErrorHandler;
use Qubus\Error\Handlers\ErrorHandler;
use Qubus\Error\Handlers\ProductionErrorHandler;
-use Qubus\Http\HttpPublisher;
-use Qubus\Http\ServerRequestFactory as ServerRequest;
+use Qubus\Http\Emitter\SapiEmitter;
+use Qubus\Http\ServerRequestFactory;
use Qubus\Routing\Router;
use function Codefy\Framework\Helpers\public_path;
use function Codefy\Framework\Helpers\router_basepath;
+use function Qubus\Config\Helpers\env;
use function Qubus\Security\Helpers\__observer;
+use function Qubus\Support\Helpers\is_null__;
use function sprintf;
use function version_compare;
@@ -39,9 +43,9 @@ public function __construct(Application $codefy, Router $router)
{
$this->codefy = $codefy;
$this->router = $router;
- $this->router->setBaseMiddleware(middleware: $this->codefy->getBaseMiddlewares());
+ $this->router->baseMiddleware = $this->codefy->getBaseMiddlewares();
$this->router->setBasePath(basePath: router_basepath(path: public_path()));
- $this->router->setDefaultNamespace(namespace: $this->codefy->getControllerNamespace());
+ $this->router->setDefaultNamespace(namespace: $this->codefy->controllerNamespace);
$this->registerErrorHandler();
}
@@ -54,26 +58,43 @@ public function codefy(): Application
/**
* @throws Exception
*/
- protected function dispatchRouter(): bool
+ protected function dispatchRouter(?ServerRequestInterface $request = null): void
{
- return (new HttpPublisher())->publish(
- content: $this->router->match(
- serverRequest: ServerRequest::fromGlobals(
- server: $_SERVER,
- query: $_GET,
- body: $_POST,
- cookies: $_COOKIE,
- files: $_FILES
- )
- ),
- emitter: new SapiEmitter()
- );
+ if (is_null__(var: $request)) {
+ $request = ServerRequestFactory::fromGlobals(
+ server: $_SERVER,
+ query: $_GET,
+ body: $_POST,
+ cookies: $_COOKIE,
+ files: $_FILES
+ );
+ }
+
+ $response = $this->handle(request: $request);
+ $responseEmitter = new SapiEmitter();
+ $responseEmitter->emit(response: $response);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $response = $this->router->match(serverRequest: $request);
+
+ $method = strtoupper($request->getMethod());
+ if ($method === 'HEAD') {
+ $emptyBody = $this->codefy->make(name: ResponseFactoryInterface::class)->createResponse()->getBody();
+ return $response->withBody($emptyBody);
+ }
+
+ return $response;
}
/**
* @throws Exception
*/
- public function boot(): bool
+ public function boot(?ServerRequestInterface $request = null): void
{
if (
version_compare(
@@ -91,13 +112,13 @@ public function boot(): bool
);
}
- __observer()->action->doAction('kernel.preboot');
+ __observer()->action->doAction('kernel_preboot');
if (! $this->codefy->hasBeenBootstrapped()) {
$this->codefy->bootstrapWith(bootstrappers: $this->bootstrappers());
}
- return $this->dispatchRouter();
+ $this->dispatchRouter($request);
}
/**
@@ -116,7 +137,7 @@ protected function bootstrappers(): array
protected function registerErrorHandler(): ErrorHandler
{
if ($this->codefy()->hasDebugModeEnabled()) {
- return new DebugErrorHandler();
+ return new DebugErrorHandler(title: env(key: 'APP_NAME', default: 'CodefyPHP') . ' Error');
}
return new ProductionErrorHandler();
diff --git a/src/Http/Middleware/ApiMiddleware.php b/src/Http/Middleware/ApiMiddleware.php
new file mode 100644
index 0000000..6fbad3e
--- /dev/null
+++ b/src/Http/Middleware/ApiMiddleware.php
@@ -0,0 +1,45 @@
+getHeaderLine('authorization'), 'Bearer ') &&
+ $request->getHeaderLine('authorization') ===
+ sprintf('Bearer %s', $this->configContainer->getConfigKey(key: 'app.api_key'))
+ ) {
+ return $handler->handle($request);
+ }
+
+ return JsonResponseFactory::create(data: 'Unauthorized.', status: 401);
+ }
+}
diff --git a/Auth/Middleware/AuthenticationMiddleware.php b/src/Http/Middleware/Auth/AuthenticationMiddleware.php
similarity index 89%
rename from Auth/Middleware/AuthenticationMiddleware.php
rename to src/Http/Middleware/Auth/AuthenticationMiddleware.php
index 2283cc6..c611761 100644
--- a/Auth/Middleware/AuthenticationMiddleware.php
+++ b/src/Http/Middleware/Auth/AuthenticationMiddleware.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Codefy\Framework\Auth\Middleware;
+namespace Codefy\Framework\Http\Middleware\Auth;
use Codefy\Framework\Auth\Sentinel;
use Psr\Http\Message\ResponseInterface;
@@ -15,7 +15,7 @@
final class AuthenticationMiddleware implements MiddlewareInterface
{
- public const AUTH_ATTRIBUTE = 'USERSESSION';
+ public const string AUTH_ATTRIBUTE = 'USERSESSION';
public function __construct(protected Sentinel $auth)
{
diff --git a/src/Http/Middleware/Auth/ExpireUserSessionMiddleware.php b/src/Http/Middleware/Auth/ExpireUserSessionMiddleware.php
new file mode 100644
index 0000000..ecc0fac
--- /dev/null
+++ b/src/Http/Middleware/Auth/ExpireUserSessionMiddleware.php
@@ -0,0 +1,59 @@
+sessionService::$options = [
+ 'cookie-name' => $this->configContainer->getConfigKey(key: 'auth.cookie_name', default: 'USERSESSID'),
+ 'cookie-lifetime' => 0,
+ ];
+ $session = $this->sessionService->makeSession($request);
+
+ /** @var UserSession $user */
+ $user = $session->get(type: UserSession::class);
+ $user
+ ->clear();
+
+ $request = $request->withAttribute(self::SESSION_ATTRIBUTE, $session);
+
+ $response = $handler->handle($request);
+
+ $response = CookiesResponse::set(
+ response: $response,
+ setCookieCollection: $this->sessionService->cookie->make(
+ name: $this->configContainer->getConfigKey(key: 'auth.cookie_name', default: 'USERSESSID'),
+ value: '',
+ maxAge: 0
+ )
+ );
+
+ return $this->sessionService->commitSession($response, $session);
+ }
+}
diff --git a/src/Http/Middleware/Auth/UserAuthorizationMiddleware.php b/src/Http/Middleware/Auth/UserAuthorizationMiddleware.php
new file mode 100644
index 0000000..9f1eddd
--- /dev/null
+++ b/src/Http/Middleware/Auth/UserAuthorizationMiddleware.php
@@ -0,0 +1,67 @@
+isLoggedIn($request))) {
+ return RedirectResponseFactory::create(
+ $this->configContainer->getConfigKey(key: 'auth.redirect_guests_to')
+ )->withAddedHeader(self::HEADER_HTTP_STATUS_CODE, 'not_authorized');
+ }
+
+ return $handler->handle($request);
+ }
+
+ /**
+ * @throws TypeException
+ * @throws Exception
+ */
+ private function isLoggedIn(ServerRequestInterface $request): bool
+ {
+ $this->sessionService::$options = [
+ 'cookie-name' => $this->configContainer->getConfigKey(key: 'auth.cookie_name', default: 'USERSESSID'),
+ ];
+ $session = $this->sessionService->makeSession($request);
+
+ /** @var UserSession $user */
+ $user = $session->get(type: UserSession::class);
+ if ($user->isEmpty()) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/Auth/Middleware/UserSessionMiddleware.php b/src/Http/Middleware/Auth/UserSessionMiddleware.php
similarity index 66%
rename from Auth/Middleware/UserSessionMiddleware.php
rename to src/Http/Middleware/Auth/UserSessionMiddleware.php
index e198e98..4e5de24 100644
--- a/Auth/Middleware/UserSessionMiddleware.php
+++ b/src/Http/Middleware/Auth/UserSessionMiddleware.php
@@ -2,9 +2,10 @@
declare(strict_types=1);
-namespace Codefy\Framework\Auth\Middleware;
+namespace Codefy\Framework\Http\Middleware\Auth;
use Codefy\Framework\Auth\UserSession;
+use Codefy\Framework\Http\Middleware\Auth\AuthenticationMiddleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
@@ -16,7 +17,7 @@
final class UserSessionMiddleware implements MiddlewareInterface
{
- public const SESSION_ATTRIBUTE = 'USERSESSION';
+ public const string SESSION_ATTRIBUTE = 'USERSESSION';
public function __construct(protected ConfigContainer $configContainer, protected SessionService $sessionService)
{
@@ -27,20 +28,21 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
try {
$userDetails = $request->getAttribute(AuthenticationMiddleware::AUTH_ATTRIBUTE);
+ $expire = isset($request->getParsedBody()['rememberme'])
+ && $request->getParsedBody()['rememberme'] === 'yes'
+ ? $this->configContainer->getConfigKey(key: 'cookies.remember')
+ : $this->configContainer->getConfigKey(key: 'cookies.lifetime');
+
$this->sessionService::$options = [
- 'cookie-name' => 'USERSESSID',
- 'cookie-lifetime' => (int) $this->configContainer->getConfigKey(
- key: 'cookies.lifetime',
- default: 86400
- ),
+ 'cookie-name' => $this->configContainer->getConfigKey(key: 'auth.cookie_name', default: 'USERSESSID'),
+ 'cookie-lifetime' => (int) $expire,
];
$session = $this->sessionService->makeSession($request);
/** @var UserSession $user */
$user = $session->get(type: UserSession::class);
$user
- ->withToken(token: $userDetails->token)
- ->withRole(role: $userDetails->role);
+ ->withToken(token: $userDetails->token);
$request = $request->withAttribute(self::SESSION_ATTRIBUTE, $user);
diff --git a/src/Http/Middleware/Cache/CacheExpiresMiddleware.php b/src/Http/Middleware/Cache/CacheExpiresMiddleware.php
new file mode 100644
index 0000000..1f808fd
--- /dev/null
+++ b/src/Http/Middleware/Cache/CacheExpiresMiddleware.php
@@ -0,0 +1,21 @@
+defaultExpires($this->configContainer->getConfigKey(key: 'http-cache.default'));
+ parent::__construct($this->configContainer->getConfigKey(key: 'http-cache.expires'));
+ }
+}
diff --git a/src/Http/Middleware/Cache/CacheMiddleware.php b/src/Http/Middleware/Cache/CacheMiddleware.php
new file mode 100644
index 0000000..a561393
--- /dev/null
+++ b/src/Http/Middleware/Cache/CacheMiddleware.php
@@ -0,0 +1,11 @@
+configContainer->getConfigKey(key: 'http-cache.types'));
+ }
+}
diff --git a/src/Http/Middleware/ContentCacheMiddleware.php b/src/Http/Middleware/ContentCacheMiddleware.php
new file mode 100644
index 0000000..9e86bd4
--- /dev/null
+++ b/src/Http/Middleware/ContentCacheMiddleware.php
@@ -0,0 +1,101 @@
+handle($request);
+
+ if (RequestMethod::GET !== $request->getMethod()) {
+ return $response;
+ }
+
+ if ($html = $this->getCachedResponseHtml($request)) {
+ return $this->buildResponse($html, $response);
+ }
+
+ if (200 === $response->getStatusCode()) {
+ $this->cacheResponse($request, $response);
+ }
+
+ return $response;
+ }
+
+ protected function buildResponse($html, ResponseInterface $response): ResponseInterface
+ {
+ $body = new Stream('php://memory', 'w');
+ $body->write($html);
+
+ return $response->withBody($body);
+ }
+
+ protected function createKeyFromRequest(RequestInterface $request): string
+ {
+ $cacheItemKeyFactory = function (RequestInterface $request): string {
+ static $key = null;
+ if (null === $key) {
+ $uri = $request->getUri();
+ $slugify = new Slugify();
+ $key = $slugify->slugify(
+ string: trim(
+ string: $uri->getPath(),
+ characters: '/'
+ ) . ($uri->getQuery() ? '?' . $uri->getQuery() : '')
+ );
+ }
+
+ return md5($key);
+ };
+
+ return $cacheItemKeyFactory($request);
+ }
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ protected function getCachedResponseHtml(RequestInterface $request)
+ {
+ return $this
+ ->cacheItemPool
+ ->getItem($this->createKeyFromRequest($request))
+ ->get();
+ }
+
+ protected function cacheResponse(RequestInterface $request, ResponseInterface $response): void
+ {
+ $cacheItem = new Item($this->createKeyFromRequest($request));
+ $value = (string) $response->getBody();
+ $cacheItem->set($value);
+
+ $this
+ ->cacheItemPool
+ ->save($cacheItem);
+ }
+}
diff --git a/src/Http/Middleware/CorsMiddleware.php b/src/Http/Middleware/CorsMiddleware.php
new file mode 100644
index 0000000..b10bf8a
--- /dev/null
+++ b/src/Http/Middleware/CorsMiddleware.php
@@ -0,0 +1,52 @@
+handle($request);
+
+ $origin = $response->getHeaderLine('origin');
+
+ if ($this->configContainer->getConfigKey(key: 'cors.access-control-allow-origin')[0] !== '*') {
+ if (
+ !$origin || !in_array(
+ $origin,
+ $this->configContainer->getConfigKey(key: 'cors.access-control-allow-origin')
+ )
+ ) {
+ return $response;
+ }
+ }
+
+ $headers = $this->configContainer->getConfigKey(key: 'cors');
+
+ foreach ($headers as $key => $value) {
+ $response = $response->withAddedHeader($key, implode(separator: ',', array: $value));
+ }
+
+ return $response;
+ }
+}
diff --git a/src/Http/Middleware/Csrf/CsrfProtectionMiddleware.php b/src/Http/Middleware/Csrf/CsrfProtectionMiddleware.php
new file mode 100644
index 0000000..c85eb85
--- /dev/null
+++ b/src/Http/Middleware/Csrf/CsrfProtectionMiddleware.php
@@ -0,0 +1,120 @@
+handle($request);
+
+ if (true === $this->needsProtection($request) && false === $this->tokensMatch($request)) {
+ return JsonResponseFactory::create(
+ data: 'Bad CSRF Token.',
+ status: $this->configContainer->getConfigKey(key: 'csrf.error_status_code')
+ );
+ }
+
+ return $response;
+ } catch (\Exception $e) {
+ return $handler->handle($request);
+ }
+ }
+
+ /**
+ * Check for methods not defined as safe.
+ *
+ * @param ServerRequestInterface $request
+ * @return bool
+ */
+ private function needsProtection(ServerRequestInterface $request): bool
+ {
+ return RequestMethod::isSafe($request->getMethod()) === false;
+ }
+
+ /**
+ * @throws Exception
+ */
+ private function tokensMatch(ServerRequestInterface $request): bool
+ {
+ $expected = $this->fetchToken($request);
+ $provided = $this->getTokenFromRequest($request);
+
+ return hash_equals($expected, $provided);
+ }
+
+
+ /**
+ * @throws Exception
+ * @throws \Exception
+ */
+ private function fetchToken(ServerRequestInterface $request): string
+ {
+ /** @var CsrfSession $csrf */
+ $csrf = $request->getAttribute(CsrfTokenMiddleware::CSRF_SESSION_ATTRIBUTE);
+
+ // Ensure the token stored previously by the CsrfTokenMiddleware is present and has a valid format.
+ if (
+ is_string($csrf->csrfToken()) &&
+ ctype_alnum($csrf->csrfToken()) &&
+ strlen($csrf->csrfToken()) === $this->configContainer->getConfigKey(key: 'csrf.csrf_token_length')
+ ) {
+ return $csrf->csrfToken();
+ }
+
+ return '';
+ }
+
+ /**
+ * @throws Exception
+ */
+ private function getTokenFromRequest(ServerRequestInterface $request): string
+ {
+ if ($request->hasHeader($this->configContainer->getConfigKey(key: 'csrf.header'))) {
+ return (string) $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header'));
+ }
+
+ // Handle the case for a POST form.
+ $body = $request->getParsedBody();
+
+ if (
+ is_array(
+ $body
+ ) &&
+ isset($body[$this->configContainer->getConfigKey(key: 'csrf.csrf_token')]) &&
+ is_string($body[$this->configContainer->getConfigKey(key: 'csrf.csrf_token')])
+ ) {
+ return $body[$this->configContainer->getConfigKey(key: 'csrf.csrf_token')];
+ }
+
+ return '';
+ }
+}
diff --git a/src/Http/Middleware/Csrf/CsrfSession.php b/src/Http/Middleware/Csrf/CsrfSession.php
new file mode 100644
index 0000000..ab02675
--- /dev/null
+++ b/src/Http/Middleware/Csrf/CsrfSession.php
@@ -0,0 +1,43 @@
+csrfToken = $csrfToken;
+
+ return $this;
+ }
+
+ public function equals(string $token): bool
+ {
+ return !is_null__($this->csrfToken) && $this->csrfToken === $token;
+ }
+
+ public function csrfToken(): string|null
+ {
+ return $this->csrfToken;
+ }
+
+ public function clear(): void
+ {
+ if (!empty($this->csrfToken)) {
+ unset($this->csrfToken);
+ }
+ }
+
+ public function isEmpty(): bool
+ {
+ return empty($this->csrfToken);
+ }
+}
diff --git a/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php b/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php
new file mode 100644
index 0000000..f01df9b
--- /dev/null
+++ b/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php
@@ -0,0 +1,98 @@
+' . "\n",
+ self::$current->getFieldAttr(),
+ self::$current->token
+ );
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function getFieldAttr(): string
+ {
+ return $this->configContainer->getConfigKey(key: 'csrf.csrf_token', default: '_token');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ try {
+ $this->sessionService::$options = [
+ 'cookie-name' => $this->configContainer->getConfigKey(key: 'csrf.cookie_name', default: 'CSRFSESSID'),
+ 'cookie-lifetime' => (int) $this->configContainer->getConfigKey(key: 'csrf.lifetime', default: 86400),
+ ];
+
+ $session = $this->sessionService->makeSession($request);
+
+ $this->token = $this->prepareToken(session: $session);
+
+ if (
+ $request->hasHeader($this->configContainer->getConfigKey(key: 'csrf.header'))
+ && $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header')) !== ''
+ ) {
+ $this->token = $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header'));
+ }
+
+ /**
+ * If true, the application will do a header check, if not,
+ * it will expect data submitted via an HTML form tag.
+ */
+ if ($this->configContainer->getConfigKey(key: 'csrf.request_header') === true) {
+ $request = $request->withHeader($this->configContainer->getConfigKey(key: 'csrf.header'), $this->token);
+ }
+
+ /** @var CsrfSession $csrf */
+ $csrf = $session->get(CsrfSession::class);
+ $csrf
+ ->withCsrfToken($this->token);
+
+ $response = $handler->handle(
+ $request
+ ->withAttribute(self::CSRF_SESSION_ATTRIBUTE, $csrf)
+ );
+
+ return $this->sessionService->commitSession($response, $session);
+ } catch (\Exception $e) {
+ return $handler->handle($request);
+ }
+ }
+}
diff --git a/src/Http/Middleware/Csrf/Traits/CsrfTokenAware.php b/src/Http/Middleware/Csrf/Traits/CsrfTokenAware.php
new file mode 100644
index 0000000..3968da3
--- /dev/null
+++ b/src/Http/Middleware/Csrf/Traits/CsrfTokenAware.php
@@ -0,0 +1,60 @@
+configContainer->getConfigKey(key: 'csrf.salt');
+
+ return sha1(string: uniqid(prefix: sha1(string: $salt), more_entropy: true));
+ }
+
+ /**
+ * @throws Exception
+ */
+ protected function prepareToken(HttpSession $session): string
+ {
+ // Try to retrieve an existing token from the session.
+ $token = $session->clientSessionId();
+
+ // If token isn't present in the session, we generate a new token.
+ if ($token === '') {
+ $token = $this->generateToken();
+ }
+ return hash_hmac(
+ algo: $this->configContainer->getConfigKey(key: 'csrf.hash_algo', default: 'sha256'),
+ data: $token,
+ key: $this->configContainer->getConfigKey(key: 'csrf.salt')
+ );
+ }
+
+ /**
+ * @throws Exception
+ */
+ protected function hashEquals(string $knownString, string $userString): bool
+ {
+ return hash_equals(
+ $knownString,
+ hash_hmac(
+ algo: $this->configContainer->getConfigKey(key: 'csrf.hash_algo', default: 'sha256'),
+ data: $userString,
+ key: $this->configContainer->getConfigKey(key: 'csrf.salt')
+ )
+ );
+ }
+}
diff --git a/src/Http/Middleware/Csrf/helpers.php b/src/Http/Middleware/Csrf/helpers.php
new file mode 100644
index 0000000..d3ffdc2
--- /dev/null
+++ b/src/Http/Middleware/Csrf/helpers.php
@@ -0,0 +1,15 @@
+debugBarRenderer = $debugBarRenderer;
+ $this->responseFactory = $responseFactory;
+ $this->streamFactory = $streamFactory ?? new StreamFactory();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ if ($staticFile = $this->getStaticFile($request->getUri())) {
+ return $staticFile;
+ }
+
+ $response = $handler->handle($request);
+
+ if ($this->shouldReturnResponse($request, $response)) {
+ return $response;
+ }
+
+ if ($this->isHtmlResponse($response)) {
+ return $this->attachDebugBarToHtmlResponse($response);
+ }
+
+ return $this->prepareHtmlResponseWithDebugBar($response);
+ }
+
+ private function shouldReturnResponse(ServerRequestInterface $request, ResponseInterface $response): bool
+ {
+ $forceHeaderValue = $request->getHeaderLine(self::FORCE_KEY);
+ $forceCookieValue = $request->getCookieParams()[self::FORCE_KEY] ?? '';
+ $forceAttributeValue = $request->getAttribute(self::FORCE_KEY, '');
+ $isForceEnable = in_array('true', [$forceHeaderValue, $forceCookieValue, $forceAttributeValue], true);
+ $isForceDisable = in_array('false', [$forceHeaderValue, $forceCookieValue, $forceAttributeValue], true);
+
+ return $isForceDisable
+ || (!$isForceEnable && ($this->isRedirect($response) || !$this->isHtmlAccepted($request)));
+ }
+
+ private function prepareHtmlResponseWithDebugBar(ResponseInterface $response): ResponseInterface
+ {
+ $head = $this->debugBarRenderer->renderHead();
+ $body = $this->debugBarRenderer->render();
+ $outResponseBody = $this->serializeResponse($response);
+ $template = '%sDebugBar
Response:
%s
%s';
+ $escapedOutResponseBody = htmlspecialchars($outResponseBody);
+ $result = sprintf($template, $head, $escapedOutResponseBody, $body);
+
+ $stream = $this->streamFactory->createStream($result);
+
+ return $this->responseFactory->createResponse()
+ ->withBody($stream)
+ ->withAddedHeader('Content-type', 'text/html');
+ }
+
+ private function attachDebugBarToHtmlResponse(ResponseInterface $response): ResponseInterface
+ {
+ $head = $this->debugBarRenderer->renderHead();
+ $body = $this->debugBarRenderer->render();
+ $responseBody = $response->getBody();
+
+ if (! $responseBody->eof() && $responseBody->isSeekable()) {
+ $responseBody->seek(0, SEEK_END);
+ }
+ $responseBody->write($head . $body);
+
+ return $response;
+ }
+
+ private function getStaticFile(UriInterface $uri): ?ResponseInterface
+ {
+ $path = $this->extractPath($uri);
+
+ if (!str_starts_with($path, $this->debugBarRenderer->getBaseUrl())) {
+ return null;
+ }
+
+ $pathToFile = substr($path, strlen($this->debugBarRenderer->getBaseUrl()));
+
+ $fullPathToFile = $this->debugBarRenderer->getBasePath() . $pathToFile;
+
+ if (!file_exists($fullPathToFile)) {
+ return null;
+ }
+
+ $contentType = $this->getContentTypeByFileName($fullPathToFile);
+ $stream = $this->streamFactory->createStreamFromResource(fopen($fullPathToFile, 'rb'));
+
+ return $this->responseFactory->createResponse()
+ ->withBody($stream)
+ ->withAddedHeader('Content-type', $contentType);
+ }
+
+ private function extractPath(UriInterface $uri): string
+ {
+ return $uri->getPath();
+ }
+
+ private function getContentTypeByFileName(string $filename): string
+ {
+ $ext = pathinfo($filename, PATHINFO_EXTENSION);
+
+ $map = [
+ 'css' => 'text/css',
+ 'js' => 'text/javascript',
+ 'otf' => 'font/opentype',
+ 'eot' => 'application/vnd.ms-fontobject',
+ 'svg' => 'image/svg+xml',
+ 'ttf' => 'application/font-sfnt',
+ 'woff' => 'application/font-woff',
+ 'woff2' => 'application/font-woff2',
+ ];
+
+ return $map[$ext] ?? 'text/plain';
+ }
+
+ private function isHtmlResponse(ResponseInterface $response): bool
+ {
+ return $this->isHtml($response, 'Content-Type');
+ }
+
+ private function isHtmlAccepted(ServerRequestInterface $request): bool
+ {
+ return $this->isHtml($request, 'Accept');
+ }
+
+ private function isHtml(MessageInterface $message, string $headerName): bool
+ {
+ return str_contains($message->getHeaderLine($headerName), 'text/html');
+ }
+
+ private function isRedirect(ResponseInterface $response): bool
+ {
+ $statusCode = $response->getStatusCode();
+
+ return $statusCode >= 300 && $statusCode < 400 && $response->getHeaderLine('Location') !== '';
+ }
+
+ private function serializeResponse(ResponseInterface $response): string
+ {
+ $reasonPhrase = $response->getReasonPhrase();
+ $headers = $this->serializeHeaders($response->getHeaders());
+ $format = 'HTTP/%s %d%s%s%s';
+
+ if (! empty($headers)) {
+ $headers = "\r\n" . $headers;
+ }
+
+ $headers .= "\r\n\r\n";
+
+ return sprintf(
+ $format,
+ $response->getProtocolVersion(),
+ $response->getStatusCode(),
+ ($reasonPhrase ? ' ' . $reasonPhrase : ''),
+ $headers,
+ $response->getBody()
+ );
+ }
+
+ /**
+ * @param array> $headers
+ */
+ private function serializeHeaders(array $headers): string
+ {
+ $lines = [];
+ foreach ($headers as $header => $values) {
+ $normalized = $this->filterHeader($header);
+ foreach ($values as $value) {
+ $lines[] = sprintf('%s: %s', $normalized, $value);
+ }
+ }
+
+ return implode("\r\n", $lines);
+ }
+
+ private function filterHeader(string $header): string
+ {
+ $filtered = str_replace('-', ' ', $header);
+ $filtered = ucwords($filtered);
+ return str_replace(' ', '-', $filtered);
+ }
+}
diff --git a/src/Http/Middleware/HtmlMinifierMiddleware.php b/src/Http/Middleware/HtmlMinifierMiddleware.php
new file mode 100644
index 0000000..70bba6c
--- /dev/null
+++ b/src/Http/Middleware/HtmlMinifierMiddleware.php
@@ -0,0 +1,17 @@
+handle($request);
+
+ $headers = new SecureHeaders($this->configContainer->getConfigKey(key: 'headers'))->headers();
+ foreach ($headers as $key => $value) {
+ $response = $response->withHeader($key, $value);
+ }
+
+ return $response;
+ }
+}
diff --git a/src/Http/Middleware/SecureHeaders/SecureHeaders.php b/src/Http/Middleware/SecureHeaders/SecureHeaders.php
new file mode 100644
index 0000000..7aafc60
--- /dev/null
+++ b/src/Http/Middleware/SecureHeaders/SecureHeaders.php
@@ -0,0 +1,327 @@
+ [],
+ 'style' => [],
+ ];
+
+ public function __construct(protected array $config = [])
+ {
+ }
+
+ public function headers(): array
+ {
+ if (! $this->compiled) {
+ $this->compile();
+ }
+
+ return $this->headers;
+ }
+
+ protected function compile(): void
+ {
+ $this->headers = array_merge(
+ $this->csp(),
+ $this->expectCT(),
+ $this->hsts(),
+ $this->permissionsPolicy(),
+ $this->miscellaneous(),
+ $this->clearSiteData(),
+ );
+
+ $this->compiled = true;
+ }
+
+ protected function csp(): array
+ {
+ if (isset($this->config['custom-csp'])) {
+ if (empty($this->config['custom-csp'])) {
+ return [];
+ }
+
+ return ['Content-Security-Policy' => $this->config['custom-csp']];
+ }
+
+ $config = $this->config['csp'] ?? [];
+
+ if (!($config['enable'] ?? false)) {
+ return [];
+ }
+
+ $config['script-src']['nonces'] = self::$nonces['script'];
+
+ $config['style-src']['nonces'] = self::$nonces['style'];
+
+ $csp = new CSPBuilder($config);
+
+ return $csp->getHeaderArray(legacy: false);
+ }
+
+ /**
+ * Strict Transport Security.
+ *
+ * @return array|string[]
+ */
+ protected function hsts(): array
+ {
+ if (! $this->config['hsts']['enable']) {
+ return [];
+ }
+
+ $hsts = sprintf('max-age=%s; preload;', $this->config['hsts']['max-age']);
+
+ if ($this->config['hsts']['include-sub-domains']) {
+ $hsts .= ' includeSubDomains;';
+ }
+
+ return ['Strict-Transport-Security' => $hsts];
+ }
+
+ protected function expectCT(): array
+ {
+ $config = $this->config['expect-ct'] ?? [];
+
+ if (!($config['enable'] ?? false)) {
+ return [];
+ }
+
+ $build[] = $this->maxAge();
+
+ if ($this->config['enforce'] ?? false) {
+ $build[] = 'enforce';
+ }
+
+ if (!empty($this->config['report-uri'])) {
+ $build[] = $this->reportUri();
+ }
+
+ return ['Expect-CT' => implode(separator: ', ', array: array_filter($build))];
+ }
+
+ /**
+ * Generate Clear-Site-Data header.
+ *
+ * @return array
+ */
+ protected function clearSiteData(): array
+ {
+ $config = $this->config['clear-site-data'] ?? [];
+
+ if (!($config['enable'] ?? false)) {
+ return [];
+ }
+
+ if ($config['all'] ?? false) {
+ return ['"*"'];
+ }
+
+ $targets = array_intersect_key($config, [
+ 'cache' => true,
+ 'cookies' => true,
+ 'storage' => true,
+ 'executionContexts' => true,
+ ]);
+
+ $needs = array_filter($targets);
+
+ $build = array_map(function (string $directive) {
+ return sprintf('"%s"', $directive);
+ }, array_keys($needs));
+
+ return ['Clear-Site-Data' => implode(separator: ', ', array: $build)];
+ }
+
+ protected function permissionsPolicy(): array
+ {
+ $config = $this->config['permissions-policy'] ?? [];
+
+ if (!($config['enable'] ?? false)) {
+ return [];
+ }
+
+ $build = [];
+
+ foreach ($config as $name => $c) {
+ if ($name === 'enable') {
+ continue;
+ }
+
+ if (empty($val = $this->directive($c))) {
+ continue;
+ }
+
+ $build[] = sprintf('%s=%s', $name, $val);
+ }
+
+ return ['Permissions-Policy' => $build];
+ }
+
+ /**
+ * Get Miscellaneous headers.
+ *
+ * @return array
+ */
+ protected function miscellaneous(): array
+ {
+ return [
+ 'X-Content-Type-Options' => $this->config['x-content-type-options'],
+ 'X-Download-Options' => $this->config['x-download-options'],
+ 'X-Frame-Options' => $this->config['x-frame-options'],
+ 'X-Permitted-Cross-Domain-Policies' => $this->config['x-permitted-cross-domain-policies'],
+ 'X-Powered-By' => $this->config['x-powered-by'] ?? ($this->config['x-power-by'] ??
+ sprintf('CodefyPHP-%s', Application::APP_VERSION)),
+ 'X-XSS-Protection' => $this->config['x-xss-protection'],
+ 'Referrer-Policy' => $this->config['referrer-policy'],
+ 'Server' => $this->config['server'],
+ 'Cross-Origin-Embedder-Policy' => $this->config['cross-origin-embedder-policy'] ?? '',
+ 'Cross-Origin-Opener-Policy' => $this->config['cross-origin-opener-policy'] ?? '',
+ 'Cross-Origin-Resource-Policy' => $this->config['cross-origin-resource-policy'] ?? '',
+ ];
+ }
+
+ protected function maxAge(): string
+ {
+ $origin = $this->config['max-age'] ?? 1800;
+
+ // convert to int
+ $age = intval(value: $origin);
+
+ // prevent negative value
+ $val = max($age, 0);
+
+ return sprintf('max-age=%d', $val);
+ }
+
+ /**
+ * Get report-uri directive.
+ */
+ protected function reportUri(): string
+ {
+ $uri = filter_var(value: $this->config['report-uri'], filter: FILTER_VALIDATE_URL);
+
+ if ($uri === false) {
+ return '';
+ }
+
+ return sprintf('report-uri="%s"', $uri);
+ }
+
+ /**
+ * Parse a specific permission policy value.
+ *
+ * @param array $config
+ * @return string
+ */
+ protected function directive(array $config): string
+ {
+ if ($config['none'] ?? false) {
+ return '()';
+ } elseif ($config['*'] ?? false) {
+ return '*';
+ }
+
+ $origins = $this->origins(origins: $config['origins'] ?? []);
+
+ if ($config['self'] ?? false) {
+ array_unshift($origins, 'self');
+ }
+
+ return sprintf('(%s)', implode(separator: ' ', array: $origins));
+ }
+
+ /**
+ * Get valid origins.
+ */
+ protected function origins(array $origins): array
+ {
+ // prevent user leave spaces by mistake
+ $trimmed = array_map(callback: 'trim', array: $origins);
+
+ // filter array using FILTER_VALIDATE_URL
+ $filters = filter_var_array(array: $trimmed, options: FILTER_VALIDATE_URL);
+
+ // get valid value
+ $passes = array_filter(array: $filters);
+
+ // ensure indexes are numerically
+ $urls = array_values(array: $passes);
+
+ return array_map(callback: function (string $url) {
+ return sprintf('"%s"', $url);
+ }, array: $urls);
+ }
+
+ /**
+ * Generate random nonce value for the current request.
+ *
+ * @throws Exception
+ */
+ public static function nonce(string $target = 'script'): string
+ {
+ $nonce = base64_encode(string: bin2hex(string: random_bytes(length: 8)));
+
+ self::$nonces[$target][] = $nonce;
+
+ return $nonce;
+ }
+
+ /**
+ * Remove a specific nonce value or flush all nonces for the given target.
+ *
+ * @param string|null $target
+ * @param string|null $nonce
+ * @return void
+ */
+ public static function removeNonce(?string $target = null, ?string $nonce = null): void
+ {
+ if ($target === null) {
+ self::$nonces['script'] = self::$nonces['style'] = [];
+ } elseif (isset(self::$nonces[$target])) {
+ if ($nonce === null) {
+ self::$nonces[$target] = [];
+ } elseif (false !== ($idx = array_search(needle: $nonce, haystack: self::$nonces[$target]))) {
+ unset(self::$nonces[$target][$idx]);
+ }
+ }
+ }
+}
diff --git a/src/Http/Middleware/Spam/HoneyPotMiddleware.php b/src/Http/Middleware/Spam/HoneyPotMiddleware.php
new file mode 100644
index 0000000..3613b79
--- /dev/null
+++ b/src/Http/Middleware/Spam/HoneyPotMiddleware.php
@@ -0,0 +1,79 @@
+attrName = $attrName;
+ self::$current = $this;
+ }
+
+ public static function getField(?string $name = null, ?string $label = null): string
+ {
+ $label = is_null__(var: $label) ? 'Honeypot Captcha' : esc_attr(string: $label);
+
+ return sprintf(
+ '' . "\n",
+ $name ?? self::$current->attrName,
+ $label
+ );
+ }
+
+ public static function getHiddenField(?string $name = null): string
+ {
+ return sprintf(
+ '' . "\n",
+ $name ?? self::$current->attrName,
+ );
+ }
+
+ /**
+ * @inheritDoc
+ * @throws Exception
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ if (!$this->isValid($request)) {
+ return JsonResponseFactory::create(
+ data: 'Form submission error.',
+ status: 403
+ );
+ }
+
+ return $handler->handle($request);
+ }
+
+ private function isValid(ServerRequestInterface $request): bool
+ {
+ $method = strtoupper(string: $request->getMethod());
+
+ if (in_array(needle: $method, haystack: ['GET', 'HEAD', 'CONNECT', 'TRACE', 'OPTIONS'], strict: true)) {
+ return true;
+ }
+
+ $data = $request->getParsedBody();
+
+ return isset($data[$this->attrName]) && $data[$this->attrName] === '';
+ }
+}
diff --git a/src/Http/Middleware/Spam/ReferrerSpamMiddleware.php b/src/Http/Middleware/Spam/ReferrerSpamMiddleware.php
new file mode 100644
index 0000000..f754f13
--- /dev/null
+++ b/src/Http/Middleware/Spam/ReferrerSpamMiddleware.php
@@ -0,0 +1,23 @@
+configContainer->getConfigKey(key: 'referrer-spam.blacklist'), $responseFactory);
+ }
+}
diff --git a/src/Http/Middleware/ThrottleMiddleware.php b/src/Http/Middleware/ThrottleMiddleware.php
new file mode 100644
index 0000000..fd4fe9d
--- /dev/null
+++ b/src/Http/Middleware/ThrottleMiddleware.php
@@ -0,0 +1,57 @@
+rateLimiter->add(
+ new Condition(
+ $this->configContainer->getConfigKey(key: 'throttle.ttl'),
+ $this->configContainer->getConfigKey(key: 'throttle.max_attempts'),
+ )
+ );
+
+ $identifier = $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header'));
+
+ try {
+ $this->rateLimiter->increment($identifier);
+ } catch (RateException $e) {
+ $condition = $e->condition;
+ return JsonResponseFactory::create(
+ data: sprintf(
+ 'You can only make %d requests in %d seconds',
+ $condition->limit,
+ $condition->ttl
+ ),
+ status: 401
+ );
+ }
+
+ return $handler->handle($request);
+ }
+}
diff --git a/src/Http/Swoole/App.php b/src/Http/Swoole/App.php
new file mode 100644
index 0000000..eb4d65e
--- /dev/null
+++ b/src/Http/Swoole/App.php
@@ -0,0 +1,90 @@
+app;
+ }
+ },
+ public private(set) ?Server $server = null {
+ get {
+ return $this->server ?? $this->app->make(name: Server::class);
+ }
+ },
+ public int $serverStartTimestamp = 0 {
+ get {
+ if ($this->serverStartTimestamp === 0) {
+ return 0;
+ }
+ return time() - $this->serverStartTimestamp;
+ }
+ }
+ ) {
+ $this->app->alias(original: App::class, alias: self::class);
+ }
+ //phpcs:enable
+
+ public function init(callable $routesCallable): void
+ {
+ $this->initRoutes(callable: $routesCallable);
+
+ $psr17 = new Psr17Factory();
+ $bridge = new BridgeManager(
+ app: $this->app,
+ responseMerger: new ResponseMerger(),
+ requestFactory: new RequestFactory(uriFactory: $psr17, streamFactory: $psr17, uploadedFileFactory: $psr17)
+ );
+
+ $this->server->on(
+ event_name: 'request',
+ callback: function (SwooleRequest $request, SwooleResponse $response) use ($bridge) {
+
+ try {
+ $response->header(key: 'X-Powered-By', value: 'Swoole + CodefyPHP');
+
+ // Boot fresh per request, ensures correct routing
+ $this->app->boot();
+
+ $bridge->process(swooleRequest: $request, swooleResponse: $response)->end();
+
+ flush();
+ } catch (\Throwable $e) {
+ $response->status(http_code: 500);
+ $response->end(content: "Internal Server Error: {$e->getMessage()}");
+ }
+ }
+ );
+ }
+
+ /**
+ * Starts Swoole Server
+ */
+ public function start(): void
+ {
+ $this->serverStartTimestamp = time();
+ $this->server->start();
+ }
+
+ private function initRoutes(callable $callable): void
+ {
+ $callable($this->app);
+ }
+}
diff --git a/src/Http/Swoole/BridgeManager.php b/src/Http/Swoole/BridgeManager.php
new file mode 100644
index 0000000..b150d75
--- /dev/null
+++ b/src/Http/Swoole/BridgeManager.php
@@ -0,0 +1,66 @@
+app = $app;
+ $this->responseMerger = $responseMerger;
+ $this->requestFactory = $requestFactory;
+ }
+
+ /**
+ * @param Request $swooleRequest
+ * @param Response $swooleResponse
+ *
+ * @return Response
+ * @throws Exception
+ */
+ public function process(
+ Request $swooleRequest,
+ Response $swooleResponse,
+ ): Response {
+
+ $psrRequest = $this->requestFactory->createServerRequest($swooleRequest);
+
+ if (!$psrRequest->getUri()->getHost() && isset($swooleRequest->header['host'])) {
+ $uri = $psrRequest->getUri()->withHost($swooleRequest->header['host']);
+ $psrRequest = $psrRequest->withUri($uri);
+ }
+
+ try {
+ $response = $this->app->handle($psrRequest);
+
+ return $this->responseMerger->toSwoole($response, $swooleResponse);
+ } catch (Exception $e) {
+ $swooleResponse->status(500);
+ $swooleResponse->end("BridgeManager error: {$e->getMessage()}");
+ return $swooleResponse;
+ }
+ }
+}
diff --git a/src/Http/Throttle/Condition.php b/src/Http/Throttle/Condition.php
new file mode 100644
index 0000000..e248d54
--- /dev/null
+++ b/src/Http/Throttle/Condition.php
@@ -0,0 +1,32 @@
+ $this->ttl;
+ set(int $value) => $this->ttl = $value;
+ },
+ public private(set) int $limit {
+ get => $this->limit;
+ set(int $value) => $this->limit = $value;
+ }
+ ) {
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString(): string
+ {
+ return (string) $this->ttl;
+ }
+}
diff --git a/src/Http/Throttle/Interval.php b/src/Http/Throttle/Interval.php
new file mode 100644
index 0000000..e8c2c0f
--- /dev/null
+++ b/src/Http/Throttle/Interval.php
@@ -0,0 +1,26 @@
+ $this->expiresAt;
+ set(int $value) => $this->expiresAt = time() + $value;
+ },
+ public int $count = 0 {
+ get => $this->count;
+ set(int $value) => $this->count = $value;
+ }
+ ) {
+ }
+}
diff --git a/src/Http/Throttle/RateException.php b/src/Http/Throttle/RateException.php
new file mode 100644
index 0000000..f1a85aa
--- /dev/null
+++ b/src/Http/Throttle/RateException.php
@@ -0,0 +1,37 @@
+condition;
+ }
+ }
+
+ /**
+ * @param string $identifier
+ * @param Condition $condition
+ */
+ public function __construct(string $identifier, Condition $condition)
+ {
+ $this->condition = $condition;
+
+ parent::__construct(
+ sprintf(
+ 'Rate %d in %d seconds was exceeded by "%s"',
+ $condition->limit,
+ $condition->ttl,
+ $identifier
+ )
+ );
+ }
+}
diff --git a/src/Http/Throttle/RateLimiter.php b/src/Http/Throttle/RateLimiter.php
new file mode 100644
index 0000000..ec808f1
--- /dev/null
+++ b/src/Http/Throttle/RateLimiter.php
@@ -0,0 +1,150 @@
+cache = $cache;
+ }
+
+ /**
+ * @param Condition $condition
+ * @return RateLimiter
+ */
+ public function add(Condition $condition): RateLimiter
+ {
+ foreach ($this->conditions as $existing) {
+ if ($condition->ttl === $existing->ttl) {
+ throw new LogicException(
+ sprintf('This instance already has a condition with a ttl of %d', $existing->ttl)
+ );
+ } elseif ($condition->ttl > $existing->ttl && $condition->limit <= $existing->limit) {
+ throw new LogicException(
+ sprintf(
+ 'Adding a condition of ttl %d, limit %d will never be
+ reached due to existing condition of ttl %d, limit %d',
+ $condition->ttl,
+ $condition->limit,
+ $existing->ttl,
+ $existing->limit
+ )
+ );
+ } elseif ($condition->ttl < $existing->ttl && $condition->limit >= $existing->limit) {
+ throw new LogicException(
+ sprintf(
+ 'Adding a condition of ttl %d, limit %d will prevent existing
+ condition of ttl %d, limit %d from being reached',
+ $condition->ttl,
+ $condition->limit,
+ $existing->ttl,
+ $existing->limit
+ )
+ );
+ }
+ }
+ $this->conditions[] = $condition;
+
+ return $this;
+ }
+
+ /**
+ * @param string $identifier
+ * @param int $count
+ * @return $this
+ * @throws RateException
+ * @throws InvalidArgumentException
+ */
+ public function increment(string $identifier, int $count = 1): RateLimiter
+ {
+ foreach ($this->conditions as $condition) {
+ $item = $this->getItem($identifier, $condition);
+ if ($item->isHit()) {
+ /** @var Interval $interval */
+ $interval = $item->get();
+ } else {
+ $interval = new Interval($condition->ttl);
+ }
+ $item->expiresAfter($condition->ttl);
+ $interval->count += $count;
+ $item->set($interval);
+ $this->cache->save($item);
+
+ if ($interval->count > $condition->limit) {
+ throw new RateException($identifier, $condition);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param string $identifier
+ * @return Interval[]
+ * @throws InvalidArgumentException
+ */
+ public function getIntervals(string $identifier): array
+ {
+ $intervals = [];
+ foreach ($this->conditions as $condition) {
+ $item = $this->getItem($identifier, $condition);
+ $interval = $item->isHit() ? $item->get() : new Interval($condition->ttl);
+ $intervals[] = $interval;
+ }
+
+ return $intervals;
+ }
+
+ /**
+ * Reset counter
+ *
+ * @param string $identifier
+ * @return $this
+ * @throws InvalidArgumentException
+ */
+ public function reset(string $identifier): RateLimiter
+ {
+ foreach ($this->conditions as $condition) {
+ $key = sprintf('%s-%s', $identifier, $condition);
+ $this->cache->deleteItem(md5($key));
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param string $identifier
+ * @param Condition $condition
+ * @return CacheItemInterface
+ * @throws InvalidArgumentException
+ */
+ private function getItem(string $identifier, Condition $condition): CacheItemInterface
+ {
+ $key = sprintf('%s-%s', $identifier, $condition);
+ return $this->cache->getItem(md5($key));
+ }
+}
diff --git a/Pipeline/Chainable.php b/src/Pipeline/Chainable.php
similarity index 100%
rename from Pipeline/Chainable.php
rename to src/Pipeline/Chainable.php
diff --git a/Pipeline/Pipeline.php b/src/Pipeline/Pipeline.php
similarity index 96%
rename from Pipeline/Pipeline.php
rename to src/Pipeline/Pipeline.php
index d72b254..5405f4a 100644
--- a/Pipeline/Pipeline.php
+++ b/src/Pipeline/Pipeline.php
@@ -7,7 +7,7 @@
use Closure;
use Codefy\Framework\Support\Traits\DbTransactionsAware;
use Exception;
-use Qubus\Inheritance\ActionAware;
+use Qubus\EventDispatcher\ActionFilter\Traits\ActionAware;
use Qubus\Injector\ServiceContainer;
use RuntimeException;
use Throwable;
@@ -109,7 +109,7 @@ public function then(Closure $destination): mixed
{
try {
$this->doAction(
- 'pipeline.started',
+ 'pipeline_started',
$destination,
$this->passable,
$this->pipes(),
@@ -129,7 +129,7 @@ public function then(Closure $destination): mixed
$this->commitTransaction();
$this->doAction(
- 'pipeline.finished',
+ 'pipeline_finished',
$destination,
$this->passable,
$this->pipes(),
@@ -197,7 +197,7 @@ protected function carry(): Closure
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
- $this->doAction('pipeline.execution.started', $pipe, $passable);
+ $this->doAction('pipeline_execution_started', $pipe, $passable);
if (is_callable($pipe)) {
// If the pipe is a callable, then we will call it directly, but otherwise we
@@ -205,7 +205,7 @@ protected function carry(): Closure
// the appropriate method and arguments, returning the results back out.
$result = $pipe($passable, $stack);
- $this->doAction('pipeline.execution.finished', $pipe, $passable);
+ $this->doAction('pipeline_execution_finished', $pipe, $passable);
return $result;
} elseif (! is_object($pipe)) {
@@ -228,7 +228,7 @@ protected function carry(): Closure
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
- $this->doAction('pipeline.execution.finished', $pipe, $passable);
+ $this->doAction('pipeline_execution_finished', $pipe, $passable);
return $this->handleCarry($carry);
};
diff --git a/Pipeline/PipelineBuilder.php b/src/Pipeline/PipelineBuilder.php
similarity index 80%
rename from Pipeline/PipelineBuilder.php
rename to src/Pipeline/PipelineBuilder.php
index d5de34e..32017a8 100644
--- a/Pipeline/PipelineBuilder.php
+++ b/src/Pipeline/PipelineBuilder.php
@@ -4,7 +4,7 @@
namespace Codefy\Framework\Pipeline;
-use Codefy\Framework\Application;
+use Codefy\Framework\Proxy\Codefy;
final class PipelineBuilder
{
@@ -24,6 +24,6 @@ public function pipe(callable $pipe): self
public function build(): Chainable
{
- return new Pipeline(Application::$APP->getContainer());
+ return new Pipeline(Codefy::$PHP->getContainer());
}
}
diff --git a/src/Pipeline/PipelineFactory.php b/src/Pipeline/PipelineFactory.php
new file mode 100644
index 0000000..1329c4a
--- /dev/null
+++ b/src/Pipeline/PipelineFactory.php
@@ -0,0 +1,22 @@
+ $pipes
+ * @return Chainable
+ */
+ public function create(iterable $pipes): Chainable
+ {
+ $builder = new PipelineBuilder();
+ foreach ($pipes as $pipe) {
+ $builder->pipe($pipe);
+ }
+
+ return $builder->build();
+ }
+}
diff --git a/src/Providers/AssetsServiceProvider.php b/src/Providers/AssetsServiceProvider.php
new file mode 100644
index 0000000..7815b1b
--- /dev/null
+++ b/src/Providers/AssetsServiceProvider.php
@@ -0,0 +1,54 @@
+codefy->configContainer->getConfigKey(key: 'assets');
+
+ // No groups defined. Assume the config is for the default group.
+ if (!isset($config['default'])) {
+ $this->registerAssetsManagerInstance(name: 'default', config: $config);
+
+ return;
+ }
+
+ // Multiple groups
+ foreach ($config as $groupName => $groupConfig) {
+ $this->registerAssetsManagerInstance($groupName, (array) $groupConfig);
+ }
+ }
+
+ /**
+ * Register an instance of the assets manager library.
+ *
+ * @param string $name Name of the group.
+ * @param array $config Config of the group.
+ *
+ * @return void
+ */
+ protected function registerAssetsManagerInstance(string $name, array $config): void
+ {
+ $this->codefy->singleton("assets.group.$name", function () use ($config) {
+
+ if (! isset($config['public_dir'])) {
+ $config['public_dir'] = public_path();
+ }
+
+ return new Assets($this->codefy, $config);
+ });
+ }
+}
diff --git a/Providers/ConfigServiceProvider.php b/src/Providers/ConfigServiceProvider.php
similarity index 85%
rename from Providers/ConfigServiceProvider.php
rename to src/Providers/ConfigServiceProvider.php
index 3cb1834..e53836c 100644
--- a/Providers/ConfigServiceProvider.php
+++ b/src/Providers/ConfigServiceProvider.php
@@ -7,11 +7,17 @@
use Codefy\Framework\Support\CodefyServiceProvider;
use Qubus\Config\Collection;
use Qubus\Config\Configuration;
+use Qubus\Config\Path\PathNotFoundException;
+use Qubus\Exception\Exception;
use function Codefy\Framework\Helpers\env;
final class ConfigServiceProvider extends CodefyServiceProvider
{
+ /**
+ * @throws PathNotFoundException
+ * @throws Exception
+ */
public function register(): void
{
$this->codefy->defineParam(
diff --git a/src/Providers/DatabaseConnectionServiceProvider.php b/src/Providers/DatabaseConnectionServiceProvider.php
new file mode 100644
index 0000000..991ec81
--- /dev/null
+++ b/src/Providers/DatabaseConnectionServiceProvider.php
@@ -0,0 +1,20 @@
+codefy->singleton(Connection::class, function () {
+ return $this->codefy->getDbConnection();
+ });
+
+ $this->codefy->share(nameOrInstance: Connection::class);
+ }
+}
diff --git a/src/Providers/EventDispatcherServiceProvider.php b/src/Providers/EventDispatcherServiceProvider.php
new file mode 100644
index 0000000..b2daafa
--- /dev/null
+++ b/src/Providers/EventDispatcherServiceProvider.php
@@ -0,0 +1,33 @@
+codefy->configContainer->getConfigKey(
+ key: 'app.event_listener',
+ default: SimpleProvider::class
+ );
+ $dispatcher = $this->codefy->configContainer->getConfigKey(
+ key: 'app.event_dispatcher',
+ default: EventDispatcher::class
+ );
+
+ $this->codefy->alias(original: ListenerProviderInterface::class, alias: $provider);
+ $this->codefy->alias(original: EventDispatcherInterface::class, alias: $dispatcher);
+ }
+}
diff --git a/Providers/FlysystemServiceProvider.php b/src/Providers/FlysystemServiceProvider.php
similarity index 100%
rename from Providers/FlysystemServiceProvider.php
rename to src/Providers/FlysystemServiceProvider.php
diff --git a/src/Providers/LocalizationServiceProvider.php b/src/Providers/LocalizationServiceProvider.php
new file mode 100644
index 0000000..c41f94d
--- /dev/null
+++ b/src/Providers/LocalizationServiceProvider.php
@@ -0,0 +1,48 @@
+codefy->isRunningInConsole()) {
+ return;
+ }
+
+ // Instantiate MoLoader for loading .mo files.
+ $loader = new MoLoader();
+ // Retrieve the current locale.
+ $locale = $this->codefy->configContainer->getConfigKey(key: 'app.locale');
+ // Retrieve the current locale domain.
+ $domain = $this->codefy->configContainer->getConfigKey(key: 'app.locale_domain');
+ // Set translation array for push.
+ $translations = [];
+ // Relative path to domain file.
+ $domainFilename = sprintf('%s/%s-%s.mo', $locale, $domain, $locale);
+ // Absolute path to the .mo file.
+ $mofile = $this->codefy->localePath() . $this->codefy::DS . $domainFilename;
+
+ if (file_exists($mofile)) {
+ $translations[] = $loader->loadFile(filename: $mofile)->setDomain(domain: $domain);
+ }
+
+ $gettext = Translator::createFromTranslations(...$translations);
+
+ TranslatorFunctions::register($gettext);
+ }
+}
diff --git a/src/Providers/PdoServiceProvider.php b/src/Providers/PdoServiceProvider.php
new file mode 100644
index 0000000..c78728b
--- /dev/null
+++ b/src/Providers/PdoServiceProvider.php
@@ -0,0 +1,20 @@
+codefy->singleton(PDO::class, function () {
+ return $this->codefy->getDbConnection()->pdo;
+ });
+
+ $this->codefy->share(nameOrInstance: PDO::class);
+ }
+}
diff --git a/src/Providers/RouterServiceProvider.php b/src/Providers/RouterServiceProvider.php
new file mode 100644
index 0000000..e789a93
--- /dev/null
+++ b/src/Providers/RouterServiceProvider.php
@@ -0,0 +1,42 @@
+codefy->singleton(key: Router::class, value: function () {
+ $router = new Router(
+ routeCollector: new RouteCollector(
+ routes: [],
+ basePath: $this->codefy->basePath(),
+ matchTypes: []
+ ),
+ container: $this->codefy,
+ responseFactory: new ResponseFactory(),
+ resolver: new InjectorMiddlewareResolver(container: $this->codefy),
+ );
+
+ $router->setDefaultNamespace(namespace: self::NAMESPACE ?? $this->codefy->controllerNamespace);
+ return $router;
+ });
+
+ $this->codefy->alias(original: Psr7Router::class, alias: Router::class);
+ $this->codefy->alias(original: 'router', alias: Router::class);
+ $this->codefy->share(nameOrInstance: Psr7Router::class);
+ $this->codefy->share(nameOrInstance: Router::class);
+ $this->codefy->share(nameOrInstance: 'router');
+ }
+}
diff --git a/src/Providers/RoutingServiceProvider.php b/src/Providers/RoutingServiceProvider.php
new file mode 100644
index 0000000..35b7be5
--- /dev/null
+++ b/src/Providers/RoutingServiceProvider.php
@@ -0,0 +1,143 @@
+booting(fn () => $this->loadRoutes());
+ }
+
+ /**
+ * Register the callback that will be used to load the application's routes.
+ *
+ * @param Closure|callable|string|array|null $routes
+ * @return $this
+ */
+ protected function routes(Closure|callable|string|array|null $routes): static
+ {
+ $this->loadRoutesUsing = $this->normalizeRoutes($routes);
+
+ return $this;
+ }
+
+ /**
+ * Register the callback that will be used to load the application's routes.
+ *
+ * @param Closure|callable|string|array|null $routes
+ * @return void
+ */
+ public static function loadRoutesUsing(Closure|callable|string|array|null $routes): void
+ {
+ static::$alwaysLoadRoutesUsing = $routes !== null
+ ? static::normalizeRoutes($routes)
+ : null;
+ }
+
+ /**
+ * Load the application routes.
+ *
+ * @return void
+ * @throws TypeException
+ */
+ protected function loadRoutes(): void
+ {
+ if (static::$alwaysLoadRoutesUsing !== null) {
+ $this->codefy->execute(static::$alwaysLoadRoutesUsing, [$this->codefy->router]);
+ }
+
+ if ($this->loadRoutesUsing !== null) {
+ $this->codefy->execute($this->loadRoutesUsing, [$this->codefy->router]);
+ }
+ }
+
+ protected static function normalizeRoutes(Closure|callable|string|array $routes): Closure
+ {
+ return function (Router $router) use ($routes): void {
+ // Handle arrays recursively
+ if (is_array($routes)) {
+ foreach ($routes as $route) {
+ $callback = $this->normalizeRoutes($route);
+ $callback($router);
+ }
+ return;
+ }
+
+ // Handle closures and callables
+ if ($routes instanceof Closure || is_callable($routes)) {
+ $routes($router);
+ return;
+ }
+
+ // Handle string (file path)
+ if (is_string($routes)) {
+ $ext = pathinfo($routes, PATHINFO_EXTENSION);
+
+ if ($ext === 'php') {
+ // PHP route file
+ if (file_exists($routes)) {
+ $result = new RouteFileRegistrar()->register($routes);
+ $result($this->codefy->router);
+ }
+ return;
+ }
+
+ if ($ext === 'json') {
+ // JSON routes
+ if (file_exists($routes)) {
+ $this->codefy->router->loadRoutesFromJson($routes);
+ }
+ return;
+ }
+ }
+
+ throw new RuntimeException(sprintf("Unsupported routes definition: %s", get_debug_type($routes)));
+ };
+ }
+
+ /**
+ * Pass dynamic methods onto the router instance.
+ *
+ * @param string $method
+ * @param array $parameters
+ * @return mixed
+ */
+ public function __call(string $method, array $parameters)
+ {
+ return $this->forwardCallTo(
+ $this->codefy->make(name: Psr7Router::class),
+ $method,
+ $parameters
+ );
+ }
+}
diff --git a/Codefy.php b/src/Proxy/Codefy.php
similarity index 75%
rename from Codefy.php
rename to src/Proxy/Codefy.php
index 26bf9a5..d026b7e 100644
--- a/Codefy.php
+++ b/src/Proxy/Codefy.php
@@ -2,8 +2,9 @@
declare(strict_types=1);
-namespace Codefy\Framework;
+namespace Codefy\Framework\Proxy;
+use Codefy\Framework\Application;
use stdClass;
class Codefy extends stdClass
diff --git a/src/Queue/NodeQueue.php b/src/Queue/NodeQueue.php
new file mode 100644
index 0000000..a8863f1
--- /dev/null
+++ b/src/Queue/NodeQueue.php
@@ -0,0 +1,366 @@
+queue = $queue;
+ $this->db = Node::open(file: $node ?? $this->queue->node());
+ }
+
+ /**
+ * @param string|callable $schedule
+ * @return bool
+ */
+ public function isDue(string|callable $schedule): bool
+ {
+ if (is_callable($schedule)) {
+ return call_user_func($schedule);
+ }
+
+ $dateTime = DateTime::createFromFormat('Y-m-d H:i:s', $schedule);
+ if ($dateTime !== false) {
+ return $dateTime->format('Y-m-d H:i') == (date('Y-m-d H:i'));
+ }
+
+ return new CronExpression((string) $schedule)->isDue();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function createItem(): string
+ {
+ $id = '';
+
+ try {
+ $id = $this->doCreateItem();
+ } catch (Exception $e) {
+ FileLoggerFactory::getLogger()->error(
+ sprintf('NODEQSTATE: %s', $e->getMessage()),
+ ['Queue' => 'NodeQueue::createItem']
+ );
+ }
+
+ return $id;
+ }
+
+ /**
+ * Adds a queue item and store it directly to the queue.
+ *
+ * @return string A unique ID if the item was successfully created and was (best effort)
+ * added to the queue, otherwise false. We don't guarantee the item was
+ * committed to disk etc, but as far as we know, the item is now in the
+ * queue.
+ * @throws TypeException
+ * @throws ReflectionException
+ */
+ protected function doCreateItem(): string
+ {
+ $lastId = '';
+
+ $query = $this->db;
+ $query->begin();
+ try {
+ $query->insert([
+ 'name' => $this->queue->name,
+ 'object' => new JsonSerializer()->serialize($this->queue),
+ 'created' => time(),
+ 'expire' => (int) 0,
+ 'executions' => (int) 0,
+ ]);
+ $query->commit();
+ $lastId = $query->lastInsertId();
+ } catch (Exception $e) {
+ $query->rollback();
+ FileLoggerFactory::getLogger()->error(
+ sprintf('NODEQSTATE: %s', $e->getMessage()),
+ ['Queue' => 'NodeQueue::doCreateItem']
+ );
+ }
+ /**
+ * Return the new serial ID, or false on failure.
+ */
+ return $lastId;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function numberOfItems(): int
+ {
+ try {
+ return count($this->db->where('name', $this->queue->name)->get());
+ } catch (Exception $e) {
+ $this->catchException($e);
+ /**
+ * If there is no node there cannot be any items.
+ */
+ return 0;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function claimItem(int $leaseTime = 3600): array|object|bool
+ {
+ /**
+ * Claim an item by updating its expiry fields. If claim is not
+ * successful another thread may have claimed the item in the meantime.
+ * Therefore, loop until an item is successfully claimed, or we are
+ * reasonably sure there are no unclaimed items left.
+ */
+ try {
+ $item = $this->db
+ ->where('expire', (int) 0)
+ ->where('name', $this->queue->name)
+ ->sortBy('created')
+ ->sortBy('_id')
+ ->first();
+ } catch (Exception $e) {
+ $this->catchException($e);
+ /**
+ * If the node does not exist there are no items currently
+ * available to claim.
+ */
+ return false;
+ }
+ if ($item) {
+ $update = $this->db;
+ $update->begin();
+ try {
+ /**
+ * Try to update the item. Only one thread can succeed in
+ * UPDATEing the same row. We cannot rely on REQUEST_TIME
+ * because items might be claimed by a single consumer which
+ * runs longer than 1 second. If we continue to use REQUEST_TIME
+ * instead of the current time(), we steal time from the lease,
+ * and will tend to reset items before the lease should really
+ * expire.
+ */
+ $update->where('expire', (int) 0)->where('_id', $item['_id'])
+ ->update([
+ 'expire' => (int) time() + (
+ $this->queue->leaseTime <= (int) 0
+ ? (int) $leaseTime
+ : (int) $this->queue->leaseTime
+ )
+ ]);
+ $update->commit();
+ return $item;
+ } catch (Exception $e) {
+ $update->rollback();
+ $this->catchException($e);
+ /**
+ * If the node does not exist there are no items currently
+ * available to claim.
+ */
+ return false;
+ }
+ } else {
+ /**
+ * No items currently available to claim.
+ */
+ return false;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function deleteItem(mixed $item): void
+ {
+ $delete = $this->db;
+ $delete->begin();
+ try {
+ $delete->where('_id', $item['_id'])
+ ->delete();
+ $delete->commit();
+ } catch (Exception $e) {
+ $delete->rollback();
+ $this->catchException($e);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function releaseItem(mixed $item): bool
+ {
+ $update = $this->db;
+ $update->begin();
+ try {
+ $update->where('_id', $item['_id'])
+ ->update([
+ 'expire' => (int) 0,
+ 'executions' => +1,
+ ]);
+ $update->commit();
+
+ return true;
+ } catch (Exception $e) {
+ $update->rollback();
+ $this->catchException($e);
+ }
+
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function createQueue()
+ {
+ /**
+ * All tasks are stored in a single node (which is created on
+ * demand) so there is nothing we need to do to create a new queue.
+ */
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function deleteQueue(): void
+ {
+ $delete = $this->db;
+ $delete->begin();
+ try {
+ $delete->where('name', $this->queue->name)
+ ->delete();
+ $delete->commit();
+ } catch (Exception $e) {
+ $delete->rollback();
+ $this->catchException($e);
+ }
+ }
+
+ /**
+ * @throws ReflectionException
+ * @throws TypeException
+ */
+ public function garbageCollection(): void
+ {
+ $delete = $this->db;
+ $delete->begin();
+ try {
+ /**
+ * Clean up the queue for failed batches.
+ */
+ $delete
+ ->where('executions', '>=', $this->queue->executions)
+ ->where('name', $this->queue->name)
+ ->delete();
+ $delete->commit();
+ } catch (Exception $e) {
+ $delete->rollback();
+ $this->catchException($e);
+ }
+
+ $update = $this->db;
+ $update->begin();
+
+ try {
+ /**
+ * Reset expired items in the default queue implementation node. If
+ * that's not used, this will simply be a no-op.
+ */
+ $update->where('expire', 'not in', (int) 0)
+ ->where('expire', '<', $_SERVER['REQUEST_TIME'])
+ ->update([
+ 'expire' => (int) 0
+ ]);
+ $update->commit();
+ } catch (Exception $e) {
+ $update->rollback();
+ $this->catchException($e);
+ }
+ }
+
+ /**
+ * Act on an exception when queue might be stale.
+ *
+ * If the node does not yet exist, that's fine, but if the node exists and
+ * yet the query failed, then the queue is stale and the exception needs to
+ * propagate.
+ *
+ * @param Exception $e The exception.
+ * @throws ReflectionException
+ * @throws TypeException
+ */
+ protected function catchException(Exception $e): void
+ {
+ FileLoggerSmtpFactory::getLogger()->error(
+ sprintf('QUEUESTATE: %s', $e->getMessage()),
+ ['Queue' => 'catchException']
+ );
+ }
+
+ /**
+ * @throws ReflectionException
+ * @throws TypeException
+ */
+ public function dispatch(): bool
+ {
+ /**
+ * Delete queues that are considered dead.
+ */
+ $this->garbageCollection();
+
+ /**
+ * Check if queue is due or not due.
+ */
+ if (!$this->isDue($this->queue->schedule)) {
+ return false;
+ }
+
+ $item = $this->claimItem();
+
+ try {
+ if (false !== $item) {
+ $object = new JsonSerializer()->unserialize($item['object']);
+
+ if (!$object instanceof ShouldQueue) {
+ $this->deleteItem($item);
+ return false;
+ };
+
+ if (false !== call_user_func([$object, 'handle'])) {
+ $this->deleteItem($item);
+ } else {
+ $this->releaseItem($item);
+ return false;
+ }
+ }
+
+ return true;
+ } catch (Exception $e) {
+ $this->catchException($e);
+ }
+
+ return false;
+ }
+}
diff --git a/src/Queue/Queue.php b/src/Queue/Queue.php
new file mode 100644
index 0000000..e67f8bc
--- /dev/null
+++ b/src/Queue/Queue.php
@@ -0,0 +1,99 @@
+options = $options + [
+ 'expression' => null,
+ 'maxRuntime' => 120,
+ 'recipients' => null,
+ 'smtpSender' => null,
+ 'smtpSenderName' => null,
+ 'enabled' => true,
+ ];
+
+ if ($new->options['expression'] instanceof Expressional) {
+ $new->expression = $new->options['expression'];
+ }
+
+ if (is_string($new->options['expression'])) {
+ $new->expression = $new->alias($new->options['expression']);
+ }
+
+ return $new;
+ }
+
+ public function withScheduler(Schedule $schedule): self
+ {
+ $new = clone $this;
+ $new->schedule = $schedule;
+
+ return $new;
+ }
+
+ public function withDispatcher(EventDispatcherInterface $dispatcher): self
+ {
+ $new = clone $this;
+ $new->dispatcher = $dispatcher;
+
+ return $new;
+ }
+
+ public function getOption(?string $option): mixed
+ {
+ if ($option === null) {
+ return $this->options;
+ }
+
+ return $this->options[$option] ?? null;
+ }
+
+ /**
+ * @throws TypeException
+ */
+ public function pid(): TaskId
+ {
+ $this->pid = TaskId::fromString();
+
+ return $this->pid;
+ }
+
+ /**
+ * Called before a task is executed.
+ */
+ public function setUp(): void
+ {
+ }
+
+ /**
+ * Executes a task.
+ */
+ abstract public function execute(Schedule $schedule): void;
+
+ /**
+ * Called after a task is executed.
+ */
+ public function tearDown(): void
+ {
+ }
+
+ /**
+ * Check if time hasn't arrived.
+ *
+ * @param string $datetime
+ * @return bool
+ */
+ protected function notYet(string $datetime): bool
+ {
+ return time() < strtotime($datetime);
+ }
+
+ /**
+ * Check if the time has passed.
+ *
+ * @param string $datetime
+ * @return bool
+ */
+ protected function past(string $datetime): bool
+ {
+ return time() > strtotime($datetime);
+ }
+
+ /**
+ * Check if event should be run.
+ *
+ * @param string $datetime
+ * @return self
+ */
+ public function from(string $datetime): self
+ {
+ return $this->skip(function () use ($datetime) {
+ return $this->notYet($datetime);
+ });
+ }
+
+ /**
+ * Check if event should not run.
+ *
+ * @param string $datetime
+ * @return self
+ */
+ public function to(string $datetime): self
+ {
+ return $this->skip(function () use ($datetime) {
+ return $this->past($datetime);
+ });
+ }
+
+ /**
+ * Checks whether the task can and should run.
+ */
+ private function shouldRun(): bool
+ {
+ if (is_false__($this->options['enabled'])) {
+ return false;
+ }
+
+ // If task overlaps don't execute.
+ if (
+ $this->canRunOnlyOneInstance() &&
+ ! $this->mutex->tryLock($this)
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function run(): bool
+ {
+ if (!$this->shouldRun()) {
+ return false;
+ }
+
+ $this->dispatcher->dispatch(event: new TaskStarted($this));
+
+ try {
+ // Run code before executing task.
+ $this->setUp();
+ // Execute task.
+ $this->execute($this->schedule);
+ // Run code after executing task.
+ $this->tearDown();
+ } catch (Exception $ex) {
+ $this->dispatcher->dispatch(event: new TaskFailed($this));
+ $this->sendEmail($ex);
+ }
+
+ $this->dispatcher->dispatch(event: new TaskCompleted($this));
+
+ return true;
+ }
+}
diff --git a/src/Scheduler/Event/TaskCompleted.php b/src/Scheduler/Event/TaskCompleted.php
new file mode 100644
index 0000000..dcfcd13
--- /dev/null
+++ b/src/Scheduler/Event/TaskCompleted.php
@@ -0,0 +1,28 @@
+ $this->task;
+ }
+
+ public function __construct(Task $task)
+ {
+ $this->task = $task;
+ }
+
+ public function getName(): string
+ {
+ return self::EVENT_NAME;
+ }
+}
diff --git a/src/Scheduler/Event/TaskFailed.php b/src/Scheduler/Event/TaskFailed.php
new file mode 100644
index 0000000..d5e18fa
--- /dev/null
+++ b/src/Scheduler/Event/TaskFailed.php
@@ -0,0 +1,28 @@
+ $this->task;
+ }
+
+ public function __construct(Task $task)
+ {
+ $this->task = $task;
+ }
+
+ public function getName(): string
+ {
+ return self::EVENT_NAME;
+ }
+}
diff --git a/src/Scheduler/Event/TaskSkipped.php b/src/Scheduler/Event/TaskSkipped.php
new file mode 100644
index 0000000..eb9dd89
--- /dev/null
+++ b/src/Scheduler/Event/TaskSkipped.php
@@ -0,0 +1,18 @@
+ $this->task;
+ }
+
+ public function __construct(Task $task)
+ {
+ $this->task = $task;
+ }
+
+ public function getName(): string
+ {
+ return self::EVENT_NAME;
+ }
+}
diff --git a/Scheduler/Expressions/At.php b/src/Scheduler/Expressions/At.php
similarity index 100%
rename from Scheduler/Expressions/At.php
rename to src/Scheduler/Expressions/At.php
diff --git a/Scheduler/Expressions/Daily.php b/src/Scheduler/Expressions/Daily.php
similarity index 100%
rename from Scheduler/Expressions/Daily.php
rename to src/Scheduler/Expressions/Daily.php
diff --git a/Scheduler/Expressions/Date.php b/src/Scheduler/Expressions/Date.php
similarity index 100%
rename from Scheduler/Expressions/Date.php
rename to src/Scheduler/Expressions/Date.php
diff --git a/Scheduler/Expressions/DayOfWeek/Friday.php b/src/Scheduler/Expressions/DayOfWeek/Friday.php
similarity index 100%
rename from Scheduler/Expressions/DayOfWeek/Friday.php
rename to src/Scheduler/Expressions/DayOfWeek/Friday.php
diff --git a/Scheduler/Expressions/DayOfWeek/Monday.php b/src/Scheduler/Expressions/DayOfWeek/Monday.php
similarity index 100%
rename from Scheduler/Expressions/DayOfWeek/Monday.php
rename to src/Scheduler/Expressions/DayOfWeek/Monday.php
diff --git a/Scheduler/Expressions/DayOfWeek/Saturday.php b/src/Scheduler/Expressions/DayOfWeek/Saturday.php
similarity index 100%
rename from Scheduler/Expressions/DayOfWeek/Saturday.php
rename to src/Scheduler/Expressions/DayOfWeek/Saturday.php
diff --git a/Scheduler/Expressions/DayOfWeek/Sunday.php b/src/Scheduler/Expressions/DayOfWeek/Sunday.php
similarity index 100%
rename from Scheduler/Expressions/DayOfWeek/Sunday.php
rename to src/Scheduler/Expressions/DayOfWeek/Sunday.php
diff --git a/Scheduler/Expressions/DayOfWeek/Thursday.php b/src/Scheduler/Expressions/DayOfWeek/Thursday.php
similarity index 100%
rename from Scheduler/Expressions/DayOfWeek/Thursday.php
rename to src/Scheduler/Expressions/DayOfWeek/Thursday.php
diff --git a/Scheduler/Expressions/DayOfWeek/Tuesday.php b/src/Scheduler/Expressions/DayOfWeek/Tuesday.php
similarity index 100%
rename from Scheduler/Expressions/DayOfWeek/Tuesday.php
rename to src/Scheduler/Expressions/DayOfWeek/Tuesday.php
diff --git a/Scheduler/Expressions/DayOfWeek/Wednesday.php b/src/Scheduler/Expressions/DayOfWeek/Wednesday.php
similarity index 100%
rename from Scheduler/Expressions/DayOfWeek/Wednesday.php
rename to src/Scheduler/Expressions/DayOfWeek/Wednesday.php
diff --git a/Scheduler/Expressions/EveryMinute.php b/src/Scheduler/Expressions/EveryMinute.php
similarity index 100%
rename from Scheduler/Expressions/EveryMinute.php
rename to src/Scheduler/Expressions/EveryMinute.php
diff --git a/Scheduler/Expressions/Expressional.php b/src/Scheduler/Expressions/Expressional.php
similarity index 100%
rename from Scheduler/Expressions/Expressional.php
rename to src/Scheduler/Expressions/Expressional.php
diff --git a/Scheduler/Expressions/Hourly.php b/src/Scheduler/Expressions/Hourly.php
similarity index 100%
rename from Scheduler/Expressions/Hourly.php
rename to src/Scheduler/Expressions/Hourly.php
diff --git a/Scheduler/Expressions/MonthOfYear/April.php b/src/Scheduler/Expressions/MonthOfYear/April.php
similarity index 100%
rename from Scheduler/Expressions/MonthOfYear/April.php
rename to src/Scheduler/Expressions/MonthOfYear/April.php
diff --git a/Scheduler/Expressions/MonthOfYear/August.php b/src/Scheduler/Expressions/MonthOfYear/August.php
similarity index 100%
rename from Scheduler/Expressions/MonthOfYear/August.php
rename to src/Scheduler/Expressions/MonthOfYear/August.php
diff --git a/Scheduler/Expressions/MonthOfYear/December.php b/src/Scheduler/Expressions/MonthOfYear/December.php
similarity index 100%
rename from Scheduler/Expressions/MonthOfYear/December.php
rename to src/Scheduler/Expressions/MonthOfYear/December.php
diff --git a/Scheduler/Expressions/MonthOfYear/February.php b/src/Scheduler/Expressions/MonthOfYear/February.php
similarity index 100%
rename from Scheduler/Expressions/MonthOfYear/February.php
rename to src/Scheduler/Expressions/MonthOfYear/February.php
diff --git a/Scheduler/Expressions/MonthOfYear/January.php b/src/Scheduler/Expressions/MonthOfYear/January.php
similarity index 100%
rename from Scheduler/Expressions/MonthOfYear/January.php
rename to src/Scheduler/Expressions/MonthOfYear/January.php
diff --git a/Scheduler/Expressions/MonthOfYear/July.php b/src/Scheduler/Expressions/MonthOfYear/July.php
similarity index 100%
rename from Scheduler/Expressions/MonthOfYear/July.php
rename to src/Scheduler/Expressions/MonthOfYear/July.php
diff --git a/Scheduler/Expressions/MonthOfYear/June.php b/src/Scheduler/Expressions/MonthOfYear/June.php
similarity index 100%
rename from Scheduler/Expressions/MonthOfYear/June.php
rename to src/Scheduler/Expressions/MonthOfYear/June.php
diff --git a/Scheduler/Expressions/MonthOfYear/March.php b/src/Scheduler/Expressions/MonthOfYear/March.php
similarity index 100%
rename from Scheduler/Expressions/MonthOfYear/March.php
rename to src/Scheduler/Expressions/MonthOfYear/March.php
diff --git a/Scheduler/Expressions/MonthOfYear/May.php b/src/Scheduler/Expressions/MonthOfYear/May.php
similarity index 100%
rename from Scheduler/Expressions/MonthOfYear/May.php
rename to src/Scheduler/Expressions/MonthOfYear/May.php
diff --git a/Scheduler/Expressions/MonthOfYear/November.php b/src/Scheduler/Expressions/MonthOfYear/November.php
similarity index 100%
rename from Scheduler/Expressions/MonthOfYear/November.php
rename to src/Scheduler/Expressions/MonthOfYear/November.php
diff --git a/Scheduler/Expressions/MonthOfYear/October.php b/src/Scheduler/Expressions/MonthOfYear/October.php
similarity index 100%
rename from Scheduler/Expressions/MonthOfYear/October.php
rename to src/Scheduler/Expressions/MonthOfYear/October.php
diff --git a/Scheduler/Expressions/MonthOfYear/September.php b/src/Scheduler/Expressions/MonthOfYear/September.php
similarity index 100%
rename from Scheduler/Expressions/MonthOfYear/September.php
rename to src/Scheduler/Expressions/MonthOfYear/September.php
diff --git a/Scheduler/Expressions/Monthly.php b/src/Scheduler/Expressions/Monthly.php
similarity index 100%
rename from Scheduler/Expressions/Monthly.php
rename to src/Scheduler/Expressions/Monthly.php
diff --git a/Scheduler/Expressions/Quarterly.php b/src/Scheduler/Expressions/Quarterly.php
similarity index 100%
rename from Scheduler/Expressions/Quarterly.php
rename to src/Scheduler/Expressions/Quarterly.php
diff --git a/Scheduler/Expressions/WeekDays.php b/src/Scheduler/Expressions/WeekDays.php
similarity index 100%
rename from Scheduler/Expressions/WeekDays.php
rename to src/Scheduler/Expressions/WeekDays.php
diff --git a/Scheduler/Expressions/WeekEnds.php b/src/Scheduler/Expressions/WeekEnds.php
similarity index 100%
rename from Scheduler/Expressions/WeekEnds.php
rename to src/Scheduler/Expressions/WeekEnds.php
diff --git a/Scheduler/Expressions/Weekly.php b/src/Scheduler/Expressions/Weekly.php
similarity index 100%
rename from Scheduler/Expressions/Weekly.php
rename to src/Scheduler/Expressions/Weekly.php
diff --git a/src/Scheduler/FailedProcessor.php b/src/Scheduler/FailedProcessor.php
new file mode 100644
index 0000000..2484ea7
--- /dev/null
+++ b/src/Scheduler/FailedProcessor.php
@@ -0,0 +1,25 @@
+ $this->processor;
+ },
+ private Exception $exception {
+ get => $this->exception;
+ }
+ ) {
+ }
+}
diff --git a/Scheduler/Mutex/CacheLocker.php b/src/Scheduler/Mutex/CacheLocker.php
similarity index 100%
rename from Scheduler/Mutex/CacheLocker.php
rename to src/Scheduler/Mutex/CacheLocker.php
diff --git a/Scheduler/Mutex/Locker.php b/src/Scheduler/Mutex/Locker.php
similarity index 100%
rename from Scheduler/Mutex/Locker.php
rename to src/Scheduler/Mutex/Locker.php
diff --git a/Scheduler/Processor/BaseProcessor.php b/src/Scheduler/Processor/BaseProcessor.php
similarity index 99%
rename from Scheduler/Processor/BaseProcessor.php
rename to src/Scheduler/Processor/BaseProcessor.php
index c6a74e5..ebd0216 100644
--- a/Scheduler/Processor/BaseProcessor.php
+++ b/src/Scheduler/Processor/BaseProcessor.php
@@ -249,6 +249,8 @@ public function maxRuntime(): int
*/
public function getExpression(): ?string
{
+ $expression = null;
+
if ($this->expression instanceof CronExpression) {
$expression = $this->expression->getExpression();
}
diff --git a/Scheduler/Processor/Callback.php b/src/Scheduler/Processor/Callback.php
similarity index 90%
rename from Scheduler/Processor/Callback.php
rename to src/Scheduler/Processor/Callback.php
index 0945e2c..bd06bbe 100644
--- a/Scheduler/Processor/Callback.php
+++ b/src/Scheduler/Processor/Callback.php
@@ -10,7 +10,7 @@
class Callback extends BaseProcessor implements Stringable, Processor
{
- public function run(): mixed
+ public function run(): string|false
{
if (
$this->preventOverlapping &&
@@ -31,7 +31,7 @@ public function run(): mixed
/**
* Executes command.
*/
- private function exec(callable $fn): mixed
+ private function exec(callable $fn): string
{
$data = $this->call($fn, $this->args, true);
diff --git a/Scheduler/Processor/Dispatcher.php b/src/Scheduler/Processor/Dispatcher.php
similarity index 94%
rename from Scheduler/Processor/Dispatcher.php
rename to src/Scheduler/Processor/Dispatcher.php
index 6aab8b5..486cf75 100644
--- a/Scheduler/Processor/Dispatcher.php
+++ b/src/Scheduler/Processor/Dispatcher.php
@@ -4,7 +4,7 @@
namespace Codefy\Framework\Scheduler\Processor;
-use Qubus\EventDispatcher\EventDispatcher;
+use Qubus\EventDispatcher\Legacy\EventDispatcher;
use Stringable;
class Dispatcher extends BaseProcessor implements Stringable, Processor
diff --git a/Scheduler/Processor/Processor.php b/src/Scheduler/Processor/Processor.php
similarity index 100%
rename from Scheduler/Processor/Processor.php
rename to src/Scheduler/Processor/Processor.php
diff --git a/Scheduler/Processor/Shell.php b/src/Scheduler/Processor/Shell.php
similarity index 100%
rename from Scheduler/Processor/Shell.php
rename to src/Scheduler/Processor/Shell.php
diff --git a/Scheduler/Schedule.php b/src/Scheduler/Schedule.php
similarity index 75%
rename from Scheduler/Schedule.php
rename to src/Scheduler/Schedule.php
index 91551dc..64c0a2e 100644
--- a/Scheduler/Schedule.php
+++ b/src/Scheduler/Schedule.php
@@ -9,9 +9,12 @@
use Codefy\Framework\Scheduler\Processor\Processor;
use Codefy\Framework\Scheduler\Processor\Shell;
use Codefy\Framework\Scheduler\Traits\LiteralAware;
+use Exception;
+use Psr\EventDispatcher\EventDispatcherInterface;
use Qubus\Exception\Data\TypeException;
-use Qubus\Exception\Exception;
use Qubus\Support\DateTime\QubusDateTimeZone;
+use ReflectionClass;
+use ReflectionException;
use function array_filter;
use function count;
@@ -27,23 +30,35 @@ class Schedule
{
use LiteralAware;
- public const SUNDAY = 0;
- public const MONDAY = 1;
- public const TUESDAY = 2;
- public const WEDNESDAY = 3;
- public const THURSDAY = 4;
- public const FRIDAY = 5;
- public const SATURDAY = 6;
+ public const int SUNDAY = 0;
+ public const int MONDAY = 1;
+ public const int TUESDAY = 2;
+ public const int WEDNESDAY = 3;
+ public const int THURSDAY = 4;
+ public const int FRIDAY = 5;
+ public const int SATURDAY = 6;
+ //phpcs:disable
/** @var Processor[] $processors */
- protected array $processors = [];
+ protected array $processors = [] {
+ &get => $this->processors;
+ }
- protected array $executedProcessors = [];
+ public protected(set) array $executedProcessors = [] {
+ &get => $this->executedProcessors;
+ }
- protected array $failedProcessors = [];
+ public protected(set) array $failedProcessors = [] {
+ &get => $this->failedProcessors;
+ }
- public function __construct(public readonly QubusDateTimeZone $timeZone, public readonly Locker $mutex)
- {
+ //phpcs:enable
+
+ public function __construct(
+ public readonly EventDispatcherInterface $dispatcher,
+ public readonly QubusDateTimeZone $timeZone,
+ public readonly Locker $mutex
+ ) {
}
/**
@@ -97,6 +112,23 @@ public function php(string $script, ?string $bin = null, array $args = []): Shel
return $command;
}
+ /**
+ * @throws ReflectionException
+ * @throws \Qubus\Exception\Exception
+ */
+ public function task(Task $task, array $options = []): BaseTask
+ {
+ $instance = new ReflectionClass($task);
+ $task = $instance->newInstanceArgs([$this->mutex, $this->timeZone]);
+ $task->withOptions($options);
+ $task->withScheduler($this);
+ $task->withDispatcher($this->dispatcher);
+
+ $this->queueProcessor($task);
+
+ return $task;
+ }
+
/**
* @return Processor[]
*/
@@ -153,16 +185,6 @@ private function pushExecutedProcessor(Processor $processor): Processor
return $processor;
}
- /**
- * Get the executed processes.
- *
- * @return array
- */
- public function getExecutedProcessors(): array
- {
- return $this->executedProcessors;
- }
-
/**
* Push a failed process.
*/
@@ -173,21 +195,13 @@ private function pushFailedProcessor(Processor $processor, Exception $ex): Proce
return $processor;
}
- /**
- * Get the failed jobs.
- *
- * @return FailedProcessor[]
- */
- public function getFailedProcessors(): array
- {
- return $this->failedProcessors;
- }
-
/**
* Compile the Task command.
*/
protected function compileArguments(array $args = []): string
{
+ $compiled = '';
+
// Sanitize command arguments.
foreach ($args as $key => $value) {
$compiled = ' ' . escapeshellarg($key);
diff --git a/src/Scheduler/Task.php b/src/Scheduler/Task.php
new file mode 100644
index 0000000..d662eea
--- /dev/null
+++ b/src/Scheduler/Task.php
@@ -0,0 +1,23 @@
+
*/
- protected $fieldsPosition = [
+ protected array $fieldsPosition = [
'minute' => CronExpression::MINUTE,
'hour' => CronExpression::HOUR,
'day' => CronExpression::DAY,
@@ -66,19 +65,11 @@ public function cron(string $expression = '* * * * *'): self
*/
public function filtersPass(): bool
{
- foreach ($this->filters as $callback) {
- if (! $this->call($callback)) {
- return false;
- }
- }
-
- foreach ($this->rejects as $callback) {
- if ($this->call($callback)) {
- return false;
- }
+ if (array_any($this->filters, fn($callback) => !$this->call($callback))) {
+ return false;
}
- return true;
+ return array_all($this->rejects, fn($callback) => !$this->call($callback));
}
/**
@@ -570,7 +561,7 @@ protected function expressionPasses(string|DateTimeZone|null $timezone = null):
$now = $now->setTimezone($timezone);
if ($this->timezone) {
- $taskTimeZone = is_object($this->timezone) && $this->timezone instanceof DateTimeZone
+ $taskTimeZone = $this->timezone instanceof DateTimeZone
? $this->timezone
->getName()
: $this->timezone;
@@ -591,14 +582,6 @@ protected function expressionPasses(string|DateTimeZone|null $timezone = null):
*/
protected function spliceIntoPosition(int $position, string $value): self
{
- /*if ($this->expression instanceof CronExpression) {
- $expression = $this->expression->getExpression();
- }
-
- if (is_string($this->expression)) {
- $expression = $this->expression;
- }*/
-
$expression = match (true) {
$this->expression instanceof CronExpression => $this->expression->getExpression(),
is_string($this->expression) => $this->expression
diff --git a/Scheduler/Traits/LiteralAware.php b/src/Scheduler/Traits/LiteralAware.php
similarity index 100%
rename from Scheduler/Traits/LiteralAware.php
rename to src/Scheduler/Traits/LiteralAware.php
diff --git a/Scheduler/Traits/MailerAware.php b/src/Scheduler/Traits/MailerAware.php
similarity index 97%
rename from Scheduler/Traits/MailerAware.php
rename to src/Scheduler/Traits/MailerAware.php
index 730fff0..3bc9e1c 100644
--- a/Scheduler/Traits/MailerAware.php
+++ b/src/Scheduler/Traits/MailerAware.php
@@ -4,7 +4,7 @@
namespace Codefy\Framework\Scheduler\Traits;
-use Qubus\Exception\Exception;
+use Exception;
use function Codefy\Framework\Helpers\app;
use function explode;
diff --git a/Scheduler/Traits/ScheduleValidateAware.php b/src/Scheduler/Traits/ScheduleValidateAware.php
similarity index 96%
rename from Scheduler/Traits/ScheduleValidateAware.php
rename to src/Scheduler/Traits/ScheduleValidateAware.php
index b0721bb..d8a8c7b 100644
--- a/Scheduler/Traits/ScheduleValidateAware.php
+++ b/src/Scheduler/Traits/ScheduleValidateAware.php
@@ -57,8 +57,7 @@ protected static function range(int|string|array|null $value = null, int $min =
}
}
- $value = implode(',', $value);
- return $value;
+ return implode(',', $value);
}
if (
diff --git a/Scheduler/ValueObject/TaskId.php b/src/Scheduler/ValueObject/TaskId.php
similarity index 100%
rename from Scheduler/ValueObject/TaskId.php
rename to src/Scheduler/ValueObject/TaskId.php
diff --git a/src/Stubs/ExampleCommand.stub b/src/Stubs/ExampleCommand.stub
new file mode 100644
index 0000000..0f7732b
--- /dev/null
+++ b/src/Stubs/ExampleCommand.stub
@@ -0,0 +1,15 @@
+ $value) {
+ if (array_key_exists($key, $defaults) && is_array($defaults[$key]) && is_array($value)) {
+ $defaults[$key] = self::deepMerge($defaults[$key], $value);
+ } else {
+ $defaults[$key] = $value;
+ }
+ }
+
+ return $defaults;
+ }
+}
diff --git a/src/Support/Assets.php b/src/Support/Assets.php
new file mode 100644
index 0000000..65f4b9c
--- /dev/null
+++ b/src/Support/Assets.php
@@ -0,0 +1,41 @@
+codefy->make($binding);
+
+ if (!$assets instanceof Assets) {
+ throw new RuntimeException(
+ message: sprintf("Assets group '%s' not found in the config file", $group)
+ );
+ }
+
+ return $assets;
+ }
+}
diff --git a/Support/BasePathDetector.php b/src/Support/BasePathDetector.php
similarity index 100%
rename from Support/BasePathDetector.php
rename to src/Support/BasePathDetector.php
diff --git a/Support/CodefyMailer.php b/src/Support/CodefyMailer.php
similarity index 75%
rename from Support/CodefyMailer.php
rename to src/Support/CodefyMailer.php
index b0b8ef5..02cbc8a 100644
--- a/Support/CodefyMailer.php
+++ b/src/Support/CodefyMailer.php
@@ -9,5 +9,5 @@
final class CodefyMailer extends QubusMailer
{
- public const VERSION = Application::APP_VERSION;
+ public const string VERSION = Application::APP_VERSION;
}
diff --git a/src/Support/CodefyServiceProvider.php b/src/Support/CodefyServiceProvider.php
new file mode 100644
index 0000000..9c637e5
--- /dev/null
+++ b/src/Support/CodefyServiceProvider.php
@@ -0,0 +1,139 @@
+> */
+ protected array $publishes = [];
+
+ /** @var array> */
+ protected array $publishGroups = [];
+
+ public function __construct(protected Application $codefy)
+ {
+ parent::__construct($codefy);
+ }
+
+ /**
+ * Register a booting callback to be run before the "boot" method is called.
+ *
+ * @param Closure $callback
+ * @return void
+ */
+ public function booting(Closure $callback): void
+ {
+ $this->bootingCallbacks[] = $callback;
+ }
+
+ /**
+ * Register a booted callback to be run after the "boot" method is called.
+ *
+ * @param Closure $callback
+ * @return void
+ */
+ public function booted(Closure $callback): void
+ {
+ $this->bootedCallbacks[] = $callback;
+ }
+
+ /**
+ * Call the registered booting callbacks.
+ *
+ * @return void
+ */
+ public function callBootingCallbacks(): void
+ {
+ $index = 0;
+
+ while ($index < count($this->bootingCallbacks)) {
+ $this->codefy->call($this->bootingCallbacks[$index]);
+
+ $index++;
+ }
+ }
+
+ /**
+ * Call the registered booted callbacks.
+ *
+ * @return void
+ */
+ public function callBootedCallbacks(): void
+ {
+ $index = 0;
+
+ while ($index < count($this->bootedCallbacks)) {
+ $this->codefy->call($this->bootedCallbacks[$index]);
+
+ $index++;
+ }
+ }
+
+ /**
+ * Get the default providers for a CodefyPHP application.
+ *
+ * @return DefaultProviders
+ */
+ public static function defaultProviders(): DefaultProviders
+ {
+ return new DefaultProviders();
+ }
+
+ /**
+ * Register publishable paths for this provider.
+ *
+ * @param array $paths [from => tag]
+ * @param string|null $group Optional tag/group name ("config", "migrations", etc.)
+ */
+ public function publishes(array $paths, ?string $group = null): void
+ {
+ foreach ($paths as $from => $tag) {
+ $tag = $group ?? $tag; // if group given, override
+ $this->publishes[$tag][$from] = $tag;
+ }
+ }
+
+ /**
+ * Get all publishable paths for this provider.
+ *
+ * @param string|null $tag Restrict to a tag (e.g. "config", "migrations")
+ * @return array [from => tag]
+ */
+ public function pathsToPublish(?string $tag = null): array
+ {
+ if ($tag !== null) {
+ return $this->publishes[$tag] ?? [];
+ }
+
+ return array_merge(...array_values($this->publishes ?: [[]]));
+ }
+
+ /**
+ * List all tags defined by this provider.
+ *
+ * @return array
+ */
+ public function publishTags(): array
+ {
+ return array_keys($this->publishes);
+ }
+}
diff --git a/src/Support/DefaultCommands.php b/src/Support/DefaultCommands.php
new file mode 100644
index 0000000..fa97703
--- /dev/null
+++ b/src/Support/DefaultCommands.php
@@ -0,0 +1,41 @@
+collection = $collection ?: [
+ \Codefy\Framework\Console\Commands\MakeCommand::class,
+ \Codefy\Framework\Console\Commands\ScheduleRunCommand::class,
+ \Codefy\Framework\Console\Commands\PasswordHashCommand::class,
+ \Codefy\Framework\Console\Commands\InitCommand::class,
+ \Codefy\Framework\Console\Commands\MigrateStatusCommand::class,
+ \Codefy\Framework\Console\Commands\MigrateCheckCommand::class,
+ \Codefy\Framework\Console\Commands\MigrateGenerateCommand::class,
+ \Codefy\Framework\Console\Commands\MigrateUpCommand::class,
+ \Codefy\Framework\Console\Commands\MigrateDownCommand::class,
+ \Codefy\Framework\Console\Commands\MigrateCommand::class,
+ \Codefy\Framework\Console\Commands\MigrateRollbackCommand::class,
+ \Codefy\Framework\Console\Commands\MigrateRedoCommand::class,
+ \Codefy\Framework\Console\Commands\ScheduleListCommand::class,
+ \Codefy\Framework\Console\Commands\ServeCommand::class,
+ \Codefy\Framework\Console\Commands\UuidCommand::class,
+ \Codefy\Framework\Console\Commands\UlidCommand::class,
+ \Codefy\Framework\Console\Commands\FlushPipelineCommand::class,
+ \Codefy\Framework\Console\Commands\VendorPublishCommand::class,
+ \Codefy\Framework\Console\Commands\GenerateEncryptionKeyCommand::class,
+ \Codefy\Framework\Console\Commands\GenerateEncryptionKeyFileCommand::class,
+ \Codefy\Framework\Console\Commands\EncryptEnvCommand::class,
+ \Codefy\Framework\Console\Commands\QueueListCommand::class,
+ \Codefy\Framework\Console\Commands\QueueRunCommand::class,
+ ];
+ }
+}
diff --git a/src/Support/DefaultProviders.php b/src/Support/DefaultProviders.php
new file mode 100644
index 0000000..a59c3fd
--- /dev/null
+++ b/src/Support/DefaultProviders.php
@@ -0,0 +1,23 @@
+collection = $collection ?: [
+ \Codefy\Framework\Providers\AssetsServiceProvider::class,
+ \Codefy\Framework\Providers\EventDispatcherServiceProvider::class,
+ \Codefy\Framework\Providers\DatabaseConnectionServiceProvider::class,
+ \Codefy\Framework\Providers\PdoServiceProvider::class,
+ \Codefy\Framework\Providers\LocalizationServiceProvider::class,
+ ];
+ }
+}
diff --git a/Support/LocalStorage.php b/src/Support/LocalStorage.php
similarity index 100%
rename from Support/LocalStorage.php
rename to src/Support/LocalStorage.php
diff --git a/Support/Password.php b/src/Support/Password.php
similarity index 61%
rename from Support/Password.php
rename to src/Support/Password.php
index 47e4eec..41ae521 100644
--- a/Support/Password.php
+++ b/src/Support/Password.php
@@ -7,7 +7,10 @@
use Qubus\Exception\Exception;
use function defined;
+use function password_algos;
+use function password_get_info;
use function password_hash;
+use function password_needs_rehash;
use function password_verify;
use function Qubus\Security\Helpers\__observer;
@@ -24,11 +27,12 @@ final class Password
*/
private static function algorithm(): string
{
- if (!defined(constant_name: 'PASSWORD_ARGON2ID')) {
- $algo = PASSWORD_BCRYPT;
- } else {
+ $algo = PASSWORD_BCRYPT;
+
+ if (defined(constant_name: 'PASSWORD_ARGON2ID')) {
$algo = PASSWORD_ARGON2ID;
}
+
/**
* Filters the password_hash() hashing algorithm.
*
@@ -45,6 +49,12 @@ private static function algorithm(): string
*/
private static function options(): array
{
+ $options = ['memory_cost' => 1 << 12, 'time_cost' => 2, 'threads' => 2];
+
+ if (self::algorithm() === '2y') {
+ $options = ['cost' => 12];
+ }
+
/**
* Filters the password_hash() options parameter.
*
@@ -52,7 +62,7 @@ private static function options(): array
*/
return __observer()->filter->applyFilter(
'password.hash.options',
- (array) ['memory_cost' => 1 << 12, 'time_cost' => 2, 'threads' => 2]
+ (array) $options
);
}
@@ -79,4 +89,38 @@ public static function verify(string $password, string $hash): bool
{
return password_verify($password, $hash);
}
+
+ /**
+ * Checks if the given hash matches the given algorithm and options provider.
+ * If not, it is assumed that the hash needs to be rehashed.
+ *
+ * @param string $hash
+ * @return bool
+ * @throws Exception
+ */
+ public static function needsRehash(string $hash): bool
+ {
+ return password_needs_rehash(hash: $hash, algo: self::algorithm(), options: self::options());
+ }
+
+ /**
+ * Get available password hashing algorithm IDs.
+ *
+ * @return array
+ */
+ public static function algos(): array
+ {
+ return password_algos();
+ }
+
+ /**
+ * Returns information about the given hash.
+ *
+ * @param string $password
+ * @return array
+ */
+ public static function getInfo(string $password): array
+ {
+ return password_get_info(hash: $password);
+ }
}
diff --git a/Support/Paths.php b/src/Support/Paths.php
similarity index 93%
rename from Support/Paths.php
rename to src/Support/Paths.php
index c793910..30e00d6 100644
--- a/Support/Paths.php
+++ b/src/Support/Paths.php
@@ -21,7 +21,11 @@
*/
final class Paths
{
- private array $paths = [];
+ //phpcs:disable
+ private array $paths = [] {
+ &get => $this->paths;
+ }
+ //phpcs:enable
/**
* @throws TypeException
diff --git a/src/Support/RequestMethod.php b/src/Support/RequestMethod.php
new file mode 100644
index 0000000..a04ad79
--- /dev/null
+++ b/src/Support/RequestMethod.php
@@ -0,0 +1,63 @@
+getHost();
+ $url = add_trailing_slash($url);
+
+ $url = concat_ws(string1: $url, string2: $path, separator: '');
+
+ return esc_url(url: $url);
+ }
+}
diff --git a/src/Support/StringParser.php b/src/Support/StringParser.php
new file mode 100644
index 0000000..3966793
--- /dev/null
+++ b/src/Support/StringParser.php
@@ -0,0 +1,66 @@
+collection = array_merge($this->collection, $collection);
+
+ return new static($this->collection);
+ }
+
+ /**
+ * Replace the given collection with other collections.
+ *
+ * @param array $replacements
+ * @return static
+ */
+ public function replace(array $replacements): static
+ {
+ $current = new Collection($this->collection);
+
+ foreach ($replacements as $from => $to) {
+ $key = $current->search($from);
+
+ $current = is_int($key) ? $current->replace([$key => $to]) : $current;
+ }
+
+ return new static($current->values()->toArray());
+ }
+
+ /**
+ * Disable the given collection.
+ *
+ * @param array $collection
+ * @return static
+ */
+ public function except(array $collection): static
+ {
+ return new static(
+ new Collection($this->collection)
+ ->reject(fn ($p) => in_array($p, $collection))
+ ->values()
+ ->toArray()
+ );
+ }
+
+ /**
+ * Convert the collection to an array.
+ *
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return $this->collection;
+ }
+}
diff --git a/Support/Traits/ContainerAware.php b/src/Support/Traits/ContainerAware.php
similarity index 85%
rename from Support/Traits/ContainerAware.php
rename to src/Support/Traits/ContainerAware.php
index c8a59bb..27cb268 100644
--- a/Support/Traits/ContainerAware.php
+++ b/src/Support/Traits/ContainerAware.php
@@ -4,7 +4,7 @@
namespace Codefy\Framework\Support\Traits;
-use Codefy\Framework\Codefy;
+use Codefy\Framework\Proxy\Codefy;
trait ContainerAware
{
diff --git a/Support/Traits/DbTransactionsAware.php b/src/Support/Traits/DbTransactionsAware.php
similarity index 77%
rename from Support/Traits/DbTransactionsAware.php
rename to src/Support/Traits/DbTransactionsAware.php
index 638be67..0b655e5 100644
--- a/Support/Traits/DbTransactionsAware.php
+++ b/src/Support/Traits/DbTransactionsAware.php
@@ -4,15 +4,19 @@
namespace Codefy\Framework\Support\Traits;
-use Codefy\Framework\Codefy;
+use Codefy\Framework\Proxy\Codefy;
use Qubus\Exception\Exception;
trait DbTransactionsAware
{
+ //phpcs:disable
/**
* Determines whether class uses transaction.
*/
- protected bool $useTransaction = false;
+ protected bool $useTransaction = false {
+ get => $this->useTransaction;
+ }
+ //phpcs:enable
/**
* Enable transaction in pipeline.
@@ -35,7 +39,7 @@ protected function beginTransaction(): void
return;
}
- Codefy::$PHP->getDB()->beginTransaction();
+ Codefy::$PHP->getDb()->beginTransaction();
}
/**
@@ -49,7 +53,7 @@ protected function commitTransaction(): void
return;
}
- Codefy::$PHP->getDB()->commit();
+ Codefy::$PHP->getDb()->commit();
}
/**
@@ -63,6 +67,6 @@ protected function rollbackTransaction(): void
return;
}
- Codefy::$PHP->getDB()->rollback();
+ Codefy::$PHP->getDb()->rollback();
}
}
diff --git a/src/Traits/LoggerAware.php b/src/Traits/LoggerAware.php
new file mode 100644
index 0000000..0dd0b38
--- /dev/null
+++ b/src/Traits/LoggerAware.php
@@ -0,0 +1,35 @@
+fenom = (new Fenom(
+ $this->fenom = new Fenom(
provider: new Provider(template_dir: $this->configContainer->getConfigKey(key: 'view.path'))
- ))->setCompileDir(
+ )->setCompileDir(
dir: $this->configContainer->getConfigKey(key: 'view.cache')
)->setOptions(options: $this->configContainer->getConfigKey(key: 'view.options'));
}
diff --git a/View/FoilView.php b/src/View/FoilView.php
similarity index 100%
rename from View/FoilView.php
rename to src/View/FoilView.php
diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php
new file mode 100644
index 0000000..463de0d
--- /dev/null
+++ b/tests/ApplicationTest.php
@@ -0,0 +1,54 @@
+charset;
+ Assert::assertEquals(expected: 'UTF-8', actual: $charset);
+});
+
+it(description: 'sets charset value.', closure: function () {
+ $app = Application::getInstance();
+ $app->charset = 'iso-8859-1';
+ Assert::assertSame(expected: 'ISO-8859-1', actual: $app->charset);
+});
+
+it(description: 'gets default locale value.', closure: function () {
+ $locale = Application::getInstance()->locale;
+ Assert::assertEquals(expected: 'en', actual: $locale);
+});
+
+it(description: 'sets locale value.', closure: function () {
+ $app = Application::getInstance();
+ $app->locale = 'es-ES';
+ Assert::assertSame(expected: 'es-ES', actual: $app->locale);
+
+ $app->withLocale(locale: 'es-ES');
+ Assert::assertSame(expected: 'es-ES', actual: $app->locale);
+});
+
+it(description: 'gets default controller namespace value.', closure: function () {
+ $namespace = Application::getInstance()->controllerNamespace;
+ Assert::assertEquals(expected: 'App\\Infrastructure\\Http\\Controllers', actual: $namespace);
+});
+
+it(description: 'sets controller namespace value.', closure: function () {
+ $app = Application::getInstance();
+ $app->controllerNamespace = 'Temp\\App\\Http\\Controllers';
+ Assert::assertSame(expected: 'Temp\\App\\Http\\Controllers', actual: $app->controllerNamespace);
+
+ $app->withControllerNamespace(namespace: 'Temp\\App\\Http\\Controllers');
+ Assert::assertSame(expected: 'Temp\\App\\Http\\Controllers', actual: $app->controllerNamespace);
+});
+
+it(description: 'gets default booted value.', closure: function () {
+ $booted = Application::getInstance()->booted;
+ Assert::assertEquals(expected: false, actual: $booted);
+});
+
+it(description: 'sets booted value.', closure: function () {
+ $app = Application::getInstance();
+ $app->setBooted(bool: true);
+ Assert::assertSame(expected: true, actual: $app->booted);
+});
diff --git a/tests/Auth/PermissionTest.php b/tests/Auth/PermissionTest.php
index 4a81b5d..11cca1c 100644
--- a/tests/Auth/PermissionTest.php
+++ b/tests/Auth/PermissionTest.php
@@ -6,35 +6,35 @@
$resource = Mockery::mock(\Codefy\Framework\Auth\Rbac\Resource\StorageResource::class);
-it('should get the permission name.', function () use ($resource) {
+it(description: 'should get the permission name.', closure: function () use ($resource) {
$permission = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission(
- permissionName: 'admin:edit',
+ name: 'admin:edit',
description: 'Edit permission.',
rbacStorageCollection: $resource
);
- Assert::assertEquals('admin:edit', $permission->getName());
+ Assert::assertEquals('admin:edit', $permission->name);
});
-it('should get permission description.', function () use ($resource) {
+it(description: 'should get permission description.', closure: function () use ($resource) {
$permission = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission(
- permissionName: 'admin:edit',
+ name: 'admin:edit',
description: 'Edit permission.',
rbacStorageCollection: $resource
);
- Assert::assertEquals('Edit permission.', $permission->getDescription());
+ Assert::assertEquals('Edit permission.', $permission->description);
});
-it('should get children.', function () use ($resource) {
+it(description: 'should get children.', closure: function () use ($resource) {
$permission2 = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission(
- permissionName: 'perm:perm2',
+ name: 'perm:perm2',
description: 'desc2',
rbacStorageCollection: $resource
);
$permission3 = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission(
- permissionName: 'perm:perm3',
+ name: 'perm:perm3',
description: 'desc3',
rbacStorageCollection: $resource
);
@@ -43,7 +43,7 @@
$resource->shouldReceive('getPermission')->andReturn($permission2, $permission3);
$permission1 = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission(
- permissionName: 'perm:perm1',
+ name: 'perm:perm1',
description: 'desc1',
rbacStorageCollection: $resource
);
@@ -64,9 +64,9 @@
Assert::assertEquals(['perm:perm3' => $permission3], $permission1->getChildren());
});
-it('should set and get rule.', function () use ($resource) {
+it(description: 'should set and get rule.', closure: function () use ($resource) {
$permission = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission(
- permissionName: 'admin:edit',
+ name: 'admin:edit',
description: 'Edit permission.',
rbacStorageCollection: $resource
);
diff --git a/tests/Auth/RbacTest.php b/tests/Auth/RbacTest.php
index 1b11d90..65bdeee 100644
--- a/tests/Auth/RbacTest.php
+++ b/tests/Auth/RbacTest.php
@@ -5,35 +5,27 @@
use Codefy\Framework\Auth\Rbac\Entity\Permission;
use Codefy\Framework\Auth\Rbac\Entity\Role;
use Codefy\Framework\Auth\Rbac\Rbac;
+use Codefy\Framework\Auth\Rbac\Resource\FileResource;
use PHPUnit\Framework\Assert;
-$resource = Mockery::mock(\Codefy\Framework\Auth\Rbac\Resource\StorageResource::class);
+$resource = new FileResource(file: 'rbac.json');
it('should create instance.', function () use ($resource) {
- $resource->shouldReceive('load');
-
new Rbac($resource);
- Assert::assertTrue(true);
+ Assert::assertTrue(condition: true);
});
it('should create role and return Role instance.', function () use ($resource) {
- $resource->shouldReceive('load');
- $resource->shouldReceive('addRole')
- ->with('role1', 'desc1')
- ->andReturn(Mockery::mock(Role::class));
-
$rbac = new Rbac($resource);
- Assert::assertInstanceOf(Role::class, $rbac->addRole(name: 'role1', description: 'desc1'));
+ Assert::assertInstanceOf(
+ expected: Role::class,
+ actual: $rbac->addRole(name: 'admin', description: 'Administrator')
+ );
});
it('should create Permission and return Permission instance.', function () use ($resource) {
- $resource->shouldReceive('load');
- $resource->shouldReceive('addPermission')
- ->with('dashboard:view', 'Access dashboard.')
- ->andReturn(Mockery::mock(Permission::class));
-
$rbac = new Rbac($resource);
Assert::assertInstanceOf(
@@ -42,44 +34,38 @@
);
});
-it('should get roles.', function () use ($resource) {
- $resource->shouldReceive('load');
- $resource->shouldReceive('getRoles')
- ->andReturn([]);
-
+it('should get empty array for roles.', function () use ($resource) {
$rbac = new Rbac($resource);
- Assert::assertEquals([], $rbac->getRoles());
+ Assert::assertEquals([], $rbac->roles);
});
it('should get role.', function () use ($resource) {
- $resource->shouldReceive('load');
- $resource->shouldReceive('getRole')
- ->with('role1')
- ->andReturn(Mockery::mock(Role::class));
-
$rbac = new Rbac($resource);
-
- Assert::assertInstanceOf(Role::class, $rbac->getRole('role1'));
+ $perm1 = $rbac->addPermission(name: 'create:post', description: 'Can create posts');
+ $perm2 = $rbac->addPermission(name: 'moderate:post', description: 'Can moderate posts');
+ $perm3 = $rbac->addPermission(name: 'update:post', description: 'Can update posts');
+ $perm4 = $rbac->addPermission(name: 'delete:post', description: 'Can delete posts');
+ $perm2->addChild($perm3);
+ $perm2->addChild($perm4);
+
+ $adminRole = $rbac->addRole(name: 'admin');
+ $moderatorRole = $rbac->addRole(name: 'moderator');
+ $authorRole = $rbac->addRole(name: 'author');
+ $adminRole->addChild($moderatorRole);
+
+ Assert::assertInstanceOf(Role::class, $rbac->getRole(name: 'admin'));
});
-it('should get permissions.', function () use ($resource) {
- $resource->shouldReceive('load');
- $resource->shouldReceive('getPermissions')
- ->andReturn([]);
-
+it('should get empty array for permissions.', function () use ($resource) {
$rbac = new Rbac($resource);
- Assert::assertEquals([], $rbac->getPermissions());
+ Assert::assertEquals(expected: [], actual: $rbac->permissions);
});
it('should get permission.', function () use ($resource) {
- $resource->shouldReceive('load');
- $resource->shouldReceive('getPermission')
- ->with('perm1')
- ->andReturn(Mockery::mock(Permission::class));
-
$rbac = new Rbac($resource);
+ $perm1 = $rbac->addPermission(name: 'create:post', description: 'Can create posts');
- Assert::assertInstanceOf(Permission::class, $rbac->getPermission(name: 'perm1'));
+ Assert::assertInstanceOf(expected: Permission::class, actual: $rbac->getPermission(name: 'create:post'));
});
diff --git a/tests/Auth/RoleTest.php b/tests/Auth/RoleTest.php
index 477633f..821129f 100644
--- a/tests/Auth/RoleTest.php
+++ b/tests/Auth/RoleTest.php
@@ -8,33 +8,33 @@
it('should create Role instance.', function () use ($resource) {
$role = new \Codefy\Framework\Auth\Rbac\Entity\RbacRole(
- roleName: 'admin',
+ name: 'admin',
description: 'Super administrator.',
rbacStorageCollection: $resource
);
- Assert::assertEquals('admin', $role->getName());
+ Assert::assertEquals('admin', $role->name);
});
it('should get the role description', function () use ($resource) {
$role = new \Codefy\Framework\Auth\Rbac\Entity\RbacRole(
- roleName: 'admin',
+ name: 'admin',
description: 'Super administrator.',
rbacStorageCollection: $resource
);
- Assert::assertEquals('Super administrator.', $role->getDescription());
+ Assert::assertEquals('Super administrator.', $role->description);
});
it('should get children.', function () use ($resource) {
$role2 = new \Codefy\Framework\Auth\Rbac\Entity\RbacRole(
- roleName: 'role2',
+ name: 'role2',
description: 'desc2',
rbacStorageCollection: $resource
);
$role3 = new \Codefy\Framework\Auth\Rbac\Entity\RbacRole(
- roleName: 'role3',
+ name: 'role3',
description: 'desc3',
rbacStorageCollection: $resource
);
@@ -43,7 +43,7 @@
$resource->shouldReceive('getRole')->andReturn($role2, $role3);
$role1 = new \Codefy\Framework\Auth\Rbac\Entity\RbacRole(
- roleName: 'role1',
+ name: 'role1',
description: 'desc1',
rbacStorageCollection: $resource
);
@@ -66,13 +66,13 @@
it('should get permissions.', function () use ($resource) {
$permission2 = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission(
- permissionName: 'perm:perm2',
+ name: 'perm:perm2',
description: 'desc2',
rbacStorageCollection: $resource
);
$permission3 = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission(
- permissionName: 'perm:perm3',
+ name: 'perm:perm3',
description: 'desc3',
rbacStorageCollection: $resource
);
@@ -81,7 +81,7 @@
$resource->shouldReceive('getPermission')->andReturn($permission2, $permission3);
$role1 = new \Codefy\Framework\Auth\Rbac\Entity\RbacRole(
- roleName: 'admin',
+ name: 'admin',
description: 'Super administrator.',
rbacStorageCollection: $resource
);
diff --git a/tests/Auth/UserSessionTest.php b/tests/Auth/UserSessionTest.php
new file mode 100644
index 0000000..1e15397
--- /dev/null
+++ b/tests/Auth/UserSessionTest.php
@@ -0,0 +1,35 @@
+token;
+
+ Assert::assertSame(expected: null, actual: $token);
+});
+
+it('should throw error when setting token property.', function () {
+ $usession = new UserSession();
+ $usession->token = 'test';
+})->throws(Error::class);
+
+it('should set token and return a UserSession object.', function () {
+ $usession = new UserSession();
+ $usession->withToken(token: '426d9e8f-b8f7-4be0-a383-324ee3c3ea4d');
+
+ Assert::assertSame(expected: '426d9e8f-b8f7-4be0-a383-324ee3c3ea4d', actual: $usession->token);
+ Assert::assertInstanceOf(expected: UserSession::class, actual: $usession);
+ Assert::assertIsObject($usession);
+});
+
+it('should be an empty token after clearing.', function () {
+ $usession = new UserSession();
+ $usession->withToken(token: '426d9e8f-b8f7-4be0-a383-324ee3c3ea4d');
+
+ Assert::assertFalse(condition: $usession->isEmpty());
+
+ $usession->clear();
+
+ Assert::assertTrue(condition: $usession->isEmpty());
+});
diff --git a/tests/Pipes/PipelineTest.php b/tests/Pipes/PipelineTest.php
index 9dcde8b..3fddfbf 100644
--- a/tests/Pipes/PipelineTest.php
+++ b/tests/Pipes/PipelineTest.php
@@ -3,8 +3,8 @@
declare(strict_types=1);
use Codefy\Framework\Application;
+use Codefy\Framework\Proxy\Codefy;
use Codefy\Framework\Pipeline\Pipeline;
-use Codefy\Framework\Pipeline\PipelineBuilder;
use Codefy\Framework\tests\Pipes\PipeFour;
use Codefy\Framework\tests\Pipes\PipeOne;
use Codefy\Framework\tests\Pipes\PipeThree;
@@ -86,8 +86,7 @@
});
it('accepts invokable class as pipe using PipelineBuilder.', function () {
- $builder = (new PipelineBuilder())
- ->pipe(new PipeFour());
+ $builder = Codefy::$PHP->pipeline->pipe(new PipeFour());
$pipeline = $builder->build();
diff --git a/tests/vendor/bootstrap.php b/tests/vendor/bootstrap.php
index 51d7213..3d103d9 100644
--- a/tests/vendor/bootstrap.php
+++ b/tests/vendor/bootstrap.php
@@ -4,7 +4,8 @@
use Qubus\Exception\Data\TypeException;
try {
- return Application::configure(['basePath' => dirname(path: __DIR__, levels: 2)])
+ return Application::create(['basePath' => dirname(path: __DIR__, levels: 2)])
+ ->withKernels()
->withProviders([
// fill in custom providers
])