diff --git a/.gitignore b/.gitignore index d62bd6a..ee42dc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ .idea +.env +config +files Scheduler/Tests vendor +.phpcs-cache .php-cs-fixer.cache .phpunit.result.cache composer.lock diff --git a/Application.php b/Application.php index a26b57c..02584b4 100644 --- a/Application.php +++ b/Application.php @@ -6,6 +6,7 @@ use Codefy\Framework\Support\Paths; use Psr\Container\ContainerInterface; +use Qubus\Config\ConfigContainer; use Qubus\Dbal\Connection; use Qubus\Dbal\DB; use Qubus\Exception\Data\TypeException; @@ -30,7 +31,7 @@ class Application extends Container { use InvokerAware; - public const APP_VERSION = '1.0.0'; + public const APP_VERSION = '1.0.9'; public const MIN_PHP_VERSION = '8.2'; @@ -74,7 +75,7 @@ public function __construct(array $params) self::$APP = $this; self::$ROOT_PATH = $this->basePath; - parent::__construct(InjectorFactory::create($this->coreAliases())); + parent::__construct(InjectorFactory::create(config: $this->coreAliases())); $this->registerDefaultServiceProviders(); } @@ -83,20 +84,21 @@ public function __construct(array $params) */ public function getDbConnection(): Connection { + /** @var $config ConfigContainer */ $config = $this->make(name: 'codefy.config'); $connection = env(key: 'DB_CONNECTION', default: 'default'); return DB::connection([ - 'driver' => $config->getConfigKey("database.connections.{$connection}.driver"), - 'host' => $config->getConfigKey("database.connections.{$connection}.host", 'localhost'), - 'port' => $config->getConfigKey("database.connections.{$connection}.port", 3306), - 'charset' => $config->getConfigKey("database.connections.{$connection}.charset", 'utf8mb4'), - 'collation' => $config->getConfigKey("database.connections.{$connection}.collation", 'utf8mb4_unicode_ci'), - 'username' => $config->getConfigKey("database.connections.{$connection}.username"), - 'password' => $config->getConfigKey("database.connections.{$connection}.password"), - 'dbname' => $config->getConfigKey("database.connections.{$connection}.dbname"), - 'prefix' => $config->getConfigKey("database.connections.{$connection}.prefix", ''), + '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"), + 'prefix' => $config->getConfigKey(key: "database.connections.{$connection}.prefix", default: ''), ]); } @@ -106,13 +108,14 @@ public function getDbConnection(): Connection */ public function getDB(): ?OrmBuilder { + /** @var $config ConfigContainer */ $config = $this->make(name: 'codefy.config'); $connection = env(key: 'DB_CONNECTION', default: 'default'); return new OrmBuilder( connection: $this->getDbConnection(), - tablePrefix: $config->getConfigKey("database.connections.{$connection}.prefix", '') + tablePrefix: $config->getConfigKey(key: "database.connections.{$connection}.prefix", default: '') ); } @@ -231,15 +234,17 @@ public function forceRegisterServiceProvider(string|Serviceable|Bootable $provid * * @return void * @throws TypeException + * @throws Exception */ public function registerConfiguredServiceProviders(): void { + /** @var $config ConfigContainer */ $config = $this->make(name: 'codefy.config'); - $providers = $config->getConfigKey('app.providers'); + $providers = $config->getConfigKey(key: 'app.providers'); foreach ($providers as $serviceProvider) { - $this->registerServiceProvider($serviceProvider); + $this->registerServiceProvider(serviceProvider: $serviceProvider); } } @@ -592,9 +597,15 @@ public function withBaseMiddlewares(array $middlewares): void $this->baseMiddlewares = $middlewares; } + /** + * @throws Exception + */ public function getBaseMiddlewares(): array { - return array_merge([], $this->baseMiddlewares); + /** @var $config ConfigContainer */ + $config = $this->make(name: 'codefy.config'); + + return array_merge($config->getConfigKey(key: 'app.base_middlewares'), $this->baseMiddlewares); } public function isRunningInConsole(): bool @@ -606,12 +617,14 @@ public function isRunningInConsole(): bool * Determine if the application is running with debug mode enabled. * * @return bool + * @throws Exception */ public function hasDebugModeEnabled(): bool { + /** @var $config ConfigContainer */ $config = $this->make(name: 'codefy.config'); - if ($config->getConfigKey('app.debug') === 'true') { + if ($config->getConfigKey(key: 'app.debug') === 'true') { return true; } @@ -627,6 +640,7 @@ protected function coreAliases(): array \Psr\Http\Message\ServerRequestInterface::class => \Qubus\Http\ServerRequest::class, \Psr\Http\Message\ServerRequestFactoryInterface::class => \Qubus\Http\ServerRequestFactory::class, \Psr\Http\Message\RequestInterface::class => \Qubus\Http\Request::class, + \Psr\Http\Server\RequestHandlerInterface::class => \Relay\Runner::class, \Psr\Http\Message\ResponseInterface::class => \Qubus\Http\Response::class, \Psr\Cache\CacheItemInterface::class => \Qubus\Cache\Psr6\Item::class, \Psr\Cache\CacheItemPoolInterface::class => \Qubus\Cache\Psr6\ItemPool::class, @@ -635,6 +649,7 @@ protected function coreAliases(): array \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' => \Qubus\Mail\Mailer::class, 'dir.path' => Support\Paths::class, 'container' => self::class, @@ -648,11 +663,18 @@ protected function coreAliases(): array \Qubus\Cache\Adapter\CacheAdapter::class => \Qubus\Cache\Adapter\FileSystemCacheAdapter::class, \Codefy\Framework\Scheduler\Mutex\Locker::class => \Codefy\Framework\Scheduler\Mutex\CacheLocker::class, + \Psr\SimpleCache\CacheInterface::class => \Qubus\Cache\Psr16\SimpleCache::class, + \Qubus\Http\Session\PhpSession::class => \Qubus\Http\Session\NativeSession::class, \DateTimeZone::class => \Qubus\Support\DateTime\QubusDateTimeZone::class, \Symfony\Component\Console\Input\InputInterface::class => \Symfony\Component\Console\Input\ArgvInput::class, \Symfony\Component\Console\Output\OutputInterface::class => \Symfony\Component\Console\Output\ConsoleOutput::class, + \Qubus\Http\Cookies\Factory\HttpCookieFactory::class + => \Qubus\Http\Cookies\Factory\CookieFactory::class, + \Qubus\Http\Session\Storage\SessionStorage::class + => \Qubus\Http\Session\Storage\SimpleCacheStorage::class, + \Qubus\Http\Session\HttpSession::class => \Qubus\Http\Session\SessionData::class, ] ]; } diff --git a/Console/ConsoleApplication.php b/Console/ConsoleApplication.php index 5acbac9..864730e 100644 --- a/Console/ConsoleApplication.php +++ b/Console/ConsoleApplication.php @@ -21,7 +21,7 @@ class ConsoleApplication extends SymfonyApplication public function __construct(protected Application $codefy) { - parent::__construct(name: 'CodefyPHP', version: '1.0.0'); + parent::__construct(name: 'CodefyPHP', version: Application::APP_VERSION); } public function run(InputInterface $input = null, OutputInterface $output = null): int diff --git a/Factory/FileLoggerSendmailFactory.php b/Factory/FileLoggerSendmailFactory.php deleted file mode 100644 index b6ed717..0000000 --- a/Factory/FileLoggerSendmailFactory.php +++ /dev/null @@ -1,51 +0,0 @@ -attach( - object: new FileLogger(filesystem: $filesystem, threshold: LogLevel::INFO) - ); - - $mail = SwiftMailerSendmailFactory::create(); - - if (env(key: 'LOGGER_FROM_EMAIL') !== null && env(key: 'LOGGER_TO_EMAIL') !== null) { - $storage->attach(object: new SwiftMailerLogger(mailer: $mail, threshold: LogLevel::INFO, params: [ - 'from' => env(key: 'LOGGER_FROM_EMAIL'), - 'to' => env(key: 'LOGGER_TO_EMAIL'), - 'subject' => env(key: 'LOGGER_EMAIL_SUBJECT'), - ])); - } - - return new Logger(loggers: $storage); - } -} diff --git a/Factory/FileLoggerSmtpFactory.php b/Factory/FileLoggerSmtpFactory.php index ede4f92..1adb034 100644 --- a/Factory/FileLoggerSmtpFactory.php +++ b/Factory/FileLoggerSmtpFactory.php @@ -9,10 +9,9 @@ use Codefy\Framework\Support\LocalStorage; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; -use Qubus\Exception\Exception; use Qubus\Log\Logger; use Qubus\Log\Loggers\FileLogger; -use Qubus\Log\Loggers\SwiftMailerLogger; +use Qubus\Log\Loggers\PHPMailerLogger; use ReflectionException; use SplObjectStorage; @@ -24,7 +23,6 @@ class FileLoggerSmtpFactory implements LoggerFactory /** * @throws ReflectionException - * @throws Exception */ public static function getLogger(): LoggerInterface { @@ -36,10 +34,10 @@ public static function getLogger(): LoggerInterface object: new FileLogger(filesystem: $filesystem, threshold: LogLevel::INFO) ); - $mail = SwiftMailerSmtpFactory::create(); + $mail = PHPMailerSmtpFactory::create(); if (env(key: 'LOGGER_FROM_EMAIL') !== null && env(key: 'LOGGER_TO_EMAIL') !== null) { - $storage->attach(object: new SwiftMailerLogger(mailer: $mail, threshold: LogLevel::INFO, params: [ + $storage->attach(object: new PHPMailerLogger(mailer: $mail, threshold: LogLevel::INFO, params: [ 'from' => env(key: 'LOGGER_FROM_EMAIL'), 'to' => env(key: 'LOGGER_TO_EMAIL'), 'subject' => env(key: 'LOGGER_EMAIL_SUBJECT'), diff --git a/Factory/SwiftMailerSmtpFactory.php b/Factory/PHPMailerSmtpFactory.php similarity index 52% rename from Factory/SwiftMailerSmtpFactory.php rename to Factory/PHPMailerSmtpFactory.php index daff105..0d484c4 100644 --- a/Factory/SwiftMailerSmtpFactory.php +++ b/Factory/PHPMailerSmtpFactory.php @@ -5,18 +5,15 @@ namespace Codefy\Framework\Factory; use Codefy\Framework\Contracts\MailerFactory; -use Qubus\Exception\Exception; +use Codefy\Framework\Support\CodefyMailer; use Qubus\Mail\Mailer; use function Codefy\Framework\Helpers\app; -class SwiftMailerSmtpFactory implements MailerFactory +class PHPMailerSmtpFactory implements MailerFactory { - /** - * @throws Exception - */ public static function create(): Mailer { - return (new Mailer())->factory(driver: 'smtp', config: app(name: 'codefy.config')); + return new CodefyMailer(config: app(name: 'codefy.config')); } } diff --git a/Factory/SwiftMailerSendmailFactory.php b/Factory/SwiftMailerSendmailFactory.php deleted file mode 100644 index 4a76b37..0000000 --- a/Factory/SwiftMailerSendmailFactory.php +++ /dev/null @@ -1,22 +0,0 @@ -factory(driver: 'sendmail', config: app(name: 'codefy.config')); - } -} diff --git a/Helpers/core.php b/Helpers/core.php index 86bc013..a68b5fd 100644 --- a/Helpers/core.php +++ b/Helpers/core.php @@ -5,15 +5,24 @@ namespace Codefy\Framework\Helpers; use Codefy\Framework\Application; +use Codefy\Framework\Factory\FileLoggerFactory; +use Codefy\Framework\Support\CodefyMailer; use Qubus\Config\Collection; use Qubus\Dbal\Connection; use Qubus\Exception\Exception; use Qubus\Expressive\OrmBuilder; +use ReflectionException; use function file_exists; +use function in_array; +use function is_string; +use function Qubus\Security\Helpers\__observer; use function Qubus\Support\Helpers\is_false__; use function Qubus\Support\Helpers\is_null__; use function rtrim; +use function sprintf; +use function substr_count; +use function ucfirst; /** * Get the available container instance. @@ -82,7 +91,7 @@ function get_fresh_bootstrap(): mixed */ function env(string $key, mixed $default = null): mixed { - return $_ENV[$key] ?? $default; + return $_ENV[$key] ?: $default; } /** @@ -106,3 +115,89 @@ function dbal(): Connection { return Application::$APP->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)) { + $instance = $instance->withCustomHeader(name: $name, value: $content); + } + } + } + + // Set X-Mailer header + $instance = $instance->withXMailer(xmailer: 'CodefyPHP Framework ' . Application::APP_VERSION); + + // 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; + } +} diff --git a/Http/BaseController.php b/Http/BaseController.php index 78e00ee..4bcfe2b 100644 --- a/Http/BaseController.php +++ b/Http/BaseController.php @@ -7,27 +7,24 @@ use Codefy\Framework\Contracts\RoutingController; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +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\app; use function Codefy\Framework\Helpers\config; class BaseController extends Controller implements RoutingController { public function __construct( - protected ?ServerRequestInterface $request = null, - protected ?ResponseInterface $response = null, - protected ?Router $router = null, + protected SessionService $sessionService, + protected ServerRequestInterface $request, + protected ResponseInterface $response, + protected Router $router, protected ?Renderer $view = null, - ) { - $this->setRequest($request ?? app(name: ServerRequestInterface::class)); - $this->response = $response ?? app(name: ResponseInterface::class); - $this->router = $router ?? app(name: 'router'); - $this->setView($view ?? new NativeLoader(config('view.path'))); + $this->setView(view: $view ?? new NativeLoader(config(key: 'view.path'))); } /** diff --git a/Http/Kernel.php b/Http/Kernel.php index 8b84a28..b60790e 100644 --- a/Http/Kernel.php +++ b/Http/Kernel.php @@ -7,6 +7,7 @@ use Codefy\Framework\Application; use Codefy\Framework\Contracts\Kernel as HttpKernel; use Exception; +use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Qubus\Error\Handlers\DebugErrorHandler; use Qubus\Error\Handlers\ErrorHandler; use Qubus\Error\Handlers\ProductionErrorHandler; @@ -62,7 +63,7 @@ protected function dispatchRouter(): bool files: $_FILES ) ), - emitter: null + emitter: new SapiEmitter() ); } diff --git a/Support/CodefyMailer.php b/Support/CodefyMailer.php new file mode 100644 index 0000000..b0b8ef5 --- /dev/null +++ b/Support/CodefyMailer.php @@ -0,0 +1,13 @@ + [ - 'public' => self::getConfigForDriverName(name: $name)['visibility']['file']['public'], - 'private' => self::getConfigForDriverName(name: $name)['visibility']['file']['private'], + 'public' => self::getConfigForDriverName(name: $name)['permission']['file']['public'], + 'private' => self::getConfigForDriverName(name: $name)['permission']['file']['private'], ], 'dir' => [ - 'public' => self::getConfigForDriverName(name: $name)['visibility']['dir']['public'], - 'private' => self::getConfigForDriverName(name: $name)['visibility']['dir']['private'], + 'public' => self::getConfigForDriverName(name: $name)['permission']['dir']['public'], + 'private' => self::getConfigForDriverName(name: $name)['permission']['dir']['private'], ], ]; } diff --git a/composer.json b/composer.json index 8e3d1a7..3231ff1 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "qubus/expressive": "^1", "qubus/filesystem": "^3", "qubus/injector": "^3", - "qubus/mail": "^3", + "qubus/mail": "^4", "qubus/router": "^3", "qubus/security": "^3", "qubus/support": "^3",