Przeglądaj źródła

fix url function parameters

michelphp 2 tygodni temu
rodzic
commit
4585da754d
4 zmienionych plików z 666 dodań i 665 usunięć
  1. 2 1
      functions/helpers.php
  2. 103 103
      src/App.php
  3. 350 350
      src/BaseKernel.php
  4. 211 211
      src/Package/MichelCorePackage.php

+ 2 - 1
functions/helpers.php

@@ -229,7 +229,7 @@ if (!function_exists('url')) {
      * @throws ContainerExceptionInterface
      * @throws NotFoundExceptionInterface
      */
-    function url(string $name, array $parameters): string
+    function url(string $name, array $parameters = []): string
     {
         /**
          * @var RouterInterface $router
@@ -244,6 +244,7 @@ if (!function_exists('asset')) {
     /**
      * Generates a URL for an asset.
      *
+     *
      * @param string $path
      * @return string The dependency injection container.
      */

+ 103 - 103
src/App.php

@@ -1,103 +1,103 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Michel\Framework\Core;
-
-use Psr\Container\ContainerInterface;
-use Psr\Http\Message\ResponseFactoryInterface;
-use Psr\Http\Message\ServerRequestFactoryInterface;
-use Psr\Http\Message\ServerRequestInterface;
-
-/**
- * @package    Michel.F
- * @author    Michel 
- * @license    https://opensource.org/license/mpl-2-0 Mozilla Public License v2.0
- */
-final class App
-{
-    private array $options;
-    private static App $instance;
-    private ?ContainerInterface $container = null;
-
-    private function __construct(array $options)
-    {
-        $required = ['server_request', 'server_request_factory', 'response_factory', 'container'];
-        foreach ($required as $key) {
-            if (!isset($options[$key]) || !$options[$key] instanceof \Closure) {
-                 throw new \InvalidArgumentException(sprintf('The option "%s" is required and must be a Closure.', $key));
-            }
-        }
-        if (isset($options['custom_environments'])) {
-             $environmentsFiltered = array_filter($options['custom_environments'], function ($value) {
-                return is_string($value) === false;
-            });
-            if ($environmentsFiltered !== []) {
-                throw new \InvalidArgumentException('custom_environments array values must be string only');
-            }
-        } else {
-            $options['custom_environments'] = [];
-        }
-
-        $this->options = $options;
-    }
-
-    public static function initWithPath(string $path): void
-    {
-        if (!file_exists($path)) {
-            throw new \InvalidArgumentException(sprintf('%s does not exist', $path));
-        }
-        self::init(require $path);
-    }
-
-    public static function init(array $options): void
-    {
-        self::$instance = new self($options);
-    }
-
-    public static function createServerRequest(): ServerRequestInterface
-    {
-        $serverRequest = self::getApp()->options['server_request'];
-        return $serverRequest();
-    }
-
-    public static function getServerRequestFactory(): ServerRequestFactoryInterface
-    {
-        $serverRequest = self::getApp()->options['server_request_factory'];
-        return $serverRequest();
-    }
-
-    public static function getResponseFactory(): ResponseFactoryInterface
-    {
-        $responseFactory = self::getApp()->options['response_factory'];
-        return $responseFactory();
-    }
-
-    public static function createContainer($definitions, $options): ContainerInterface
-    {
-        if (self::getApp()->container instanceof ContainerInterface) {
-            throw new \LogicException('A container has already been built in ' . self::class);
-        }
-        self::getApp()->container = self::getApp()->options['container']($definitions, $options);
-
-        return self::getContainer();
-    }
-
-    public static function getContainer(): ContainerInterface
-    {
-        return self::getApp()->container;
-    }
-
-    public static function getCustomEnvironments(): array
-    {
-        return self::getApp()->options['custom_environments'];
-    }
-
-    private static function getApp(): self
-    {
-        if (self::$instance === null) {
-            throw new \LogicException('Please call ::init() method before get ' . self::class);
-        }
-        return self::$instance;
-    }
-}
+<?php
+
+declare(strict_types=1);
+
+namespace Michel\Framework\Core;
+
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ServerRequestFactoryInterface;
+use Psr\Http\Message\ServerRequestInterface;
+
+/**
+ * @package    Michel.F
+ * @author    Michel 
+ * @license    https://opensource.org/license/mpl-2-0 Mozilla Public License v2.0
+ */
+final class App
+{
+    private array $options;
+    private static App $instance;
+    private ?ContainerInterface $container = null;
+
+    private function __construct(array $options)
+    {
+        $required = ['server_request', 'server_request_factory', 'response_factory', 'container'];
+        foreach ($required as $key) {
+            if (!isset($options[$key]) || !$options[$key] instanceof \Closure) {
+                 throw new \InvalidArgumentException(sprintf('The option "%s" is required and must be a Closure.', $key));
+            }
+        }
+        if (isset($options['custom_environments'])) {
+             $environmentsFiltered = array_filter($options['custom_environments'], function ($value) {
+                return is_string($value) === false;
+            });
+            if ($environmentsFiltered !== []) {
+                throw new \InvalidArgumentException('custom_environments array values must be string only');
+            }
+        } else {
+            $options['custom_environments'] = [];
+        }
+
+        $this->options = $options;
+    }
+
+    public static function initWithPath(string $path): void
+    {
+        if (!file_exists($path)) {
+            throw new \InvalidArgumentException(sprintf('%s does not exist', $path));
+        }
+        self::init(require $path);
+    }
+
+    public static function init(array $options): void
+    {
+        self::$instance = new self($options);
+    }
+
+    public static function createServerRequest(): ServerRequestInterface
+    {
+        $serverRequest = self::getApp()->options['server_request'];
+        return $serverRequest();
+    }
+
+    public static function getServerRequestFactory(): ServerRequestFactoryInterface
+    {
+        $serverRequest = self::getApp()->options['server_request_factory'];
+        return $serverRequest();
+    }
+
+    public static function getResponseFactory(): ResponseFactoryInterface
+    {
+        $responseFactory = self::getApp()->options['response_factory'];
+        return $responseFactory();
+    }
+
+    public static function createContainer($definitions, $options): ContainerInterface
+    {
+        if (self::getApp()->container instanceof ContainerInterface) {
+            throw new \LogicException('A container has already been built in ' . self::class);
+        }
+        self::getApp()->container = self::getApp()->options['container']($definitions, $options);
+
+        return self::getContainer();
+    }
+
+    public static function getContainer(): ContainerInterface
+    {
+        return self::getApp()->container;
+    }
+
+    public static function getCustomEnvironments(): array
+    {
+        return self::getApp()->options['custom_environments'];
+    }
+
+    private static function getApp(): self
+    {
+        if (self::$instance === null) {
+            throw new \LogicException('Please call ::init() method before get ' . self::class);
+        }
+        return self::$instance;
+    }
+}

+ 350 - 350
src/BaseKernel.php

@@ -1,350 +1,350 @@
-<?php
-declare(strict_types=1);
-
-namespace Michel\Framework\Core;
-
-use DateTimeImmutable;
-use Michel\Attribute\AttributeRouteCollector;
-use Michel\Env\DotEnv;
-use Michel\Framework\Core\Debug\DebugDataCollector;
-use Michel\Framework\Core\ErrorHandler\ErrorHandler;
-use Michel\Framework\Core\ErrorHandler\ExceptionHandler;
-use Michel\Framework\Core\Handler\RequestHandler;
-use Michel\Framework\Core\Http\Exception\HttpException;
-use Michel\Framework\Core\Http\Exception\HttpExceptionInterface;
-use InvalidArgumentException;
-use Michel\Framework\Core\Finder\ControllerFinder;
-use Michel\Framework\Core\Http\RequestContext;
-use Michel\Package\PackageInterface;
-use Psr\Container\ContainerInterface;
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface;
-use Psr\Http\Server\MiddlewareInterface;
-use Throwable;
-use function array_filter;
-use function array_keys;
-use function array_merge;
-use function date_default_timezone_set;
-use function error_reporting;
-use function getenv;
-use function implode;
-use function in_array;
-use function json_encode;
-use function sprintf;
-
-/**
- * @package    Michel.F
- * @author    Michel 
- * @license    https://opensource.org/license/mpl-2-0 Mozilla Public License v2.0
- */
-abstract class BaseKernel
-{
-    private const DEFAULT_ENV = 'prod';
-    public const VERSION = '0.0.1-alpha';
-    public const NAME = 'MICHEL';
-    private const DEFAULT_ENVIRONMENTS = [
-        'dev',
-        'prod'
-    ];
-    private string $env = self::DEFAULT_ENV;
-    private bool $debug = false;
-
-    protected ContainerInterface $container;
-    /**
-     * @var array<MiddlewareInterface>|array<string>
-     */
-    private array $middlewareCollection = [];
-    private ?DebugDataCollector $debugDataCollector = null;
-
-    /**
-     * BaseKernel constructor.
-     */
-    public function __construct()
-    {
-        App::init($this->loadConfigurationIfExists('framework.php'));
-        $this->boot();
-    }
-
-    /**
-     * @param ServerRequestInterface $request
-     * @return ResponseInterface
-     * @throws Throwable
-     */
-    final public function handle(ServerRequestInterface $request): ResponseInterface
-    {
-        try {
-            $request = $request->withAttribute('request_id', strtoupper(uniqid('REQ')));
-            $request = $request->withAttribute('debug_collector', $this->debugDataCollector);
-
-            /**
-             * @var RequestContext $context
-             */
-            $context = $this->container->get(RequestContext::class);
-            $context->setRequest($request);
-
-            $requestHandler = new RequestHandler($this->container, $this->middlewareCollection);
-            $response =  $requestHandler->handle($request);
-            if ($response->getStatusCode() >= 400 && $response->getStatusCode() < 600) {
-                throw new HttpException($response->getStatusCode(), $response->getReasonPhrase());
-            }
-            return $response;
-        } catch (Throwable $exception) {
-            if (!$exception instanceof HttpExceptionInterface) {
-                $this->logException($exception, $request);
-            }
-
-            $exceptionHandler = $this->container->get(ExceptionHandler::class);
-            return $exceptionHandler->render($request, $exception);
-        }
-    }
-
-    final public function getEnv(): string
-    {
-        return $this->env;
-    }
-
-    final public function isDebug(): bool
-    {
-        return $this->debug;
-    }
-
-    final public function getContainer(): ContainerInterface
-    {
-        return $this->container;
-    }
-
-    abstract public function getProjectDir(): string;
-
-    abstract public function getCacheDir(): string;
-
-    abstract public function getLogDir(): string;
-
-    abstract public function getConfigDir(): string;
-
-    abstract public function getPublicDir(): string;
-
-    abstract public function getEnvFile(): string;
-
-    abstract protected function afterBoot(): void;
-
-    protected function loadContainer(array $definitions): ContainerInterface
-    {
-        return App::createContainer($definitions, ['cache_dir' => $this->getCacheDir()]);
-    }
-
-    final protected function logException(Throwable $exception, ServerRequestInterface $request): void
-    {
-        $this->log([
-            '@timestamp' => (new DateTimeImmutable())->format('c'),
-            'log.level' => 'error',
-            'id' => $request->getAttribute('request_id'),
-            'message' => $exception->getMessage(),
-            'http.request' => [
-                'method' => $request->getMethod(),
-                'url' => $request->getUri()->__toString(),
-            ],
-            'error' => [
-                'code' => $exception->getCode(),
-                'stack_trace' => $exception->getTrace(),
-                'class' => get_class($exception),
-            ],
-            'source' => [
-                'file' => $exception->getFile(),
-                'line' => $exception->getLine(),
-            ],
-        ], sprintf('%s_request_error.log', $this->getEnv()));
-    }
-
-    final protected function log(array $data, string $logFile = null): void
-    {
-        $logDir = $this->getLogDir();
-        if (empty($logDir)) {
-            throw new InvalidArgumentException('The log dir is empty, please set it in the Kernel.');
-        }
-
-        if (!is_dir($logDir) && !mkdir($logDir, 0777, true) && !is_dir($logDir)) {
-            throw new \RuntimeException(sprintf('Directory "%s" was not created', $logDir));
-        }
-        if ($logFile === null) {
-            $logFile = $this->getEnv() . '.log';
-        }
-        error_log(
-            json_encode($data, JSON_UNESCAPED_SLASHES) . PHP_EOL,
-            3,
-            filepath_join( $logDir, $logFile)
-        );
-    }
-
-    private function boot(): void
-    {
-        $this->initEnv();
-        $this->configureErrorHandling();
-        $this->configureTimezone();
-
-        $middleware = $this->loadConfigurationIfExists('middleware.php');
-        $middleware = array_filter($middleware, function ($environments) {
-            return in_array($this->getEnv(), $environments);
-        });
-        $this->middlewareCollection = array_keys($middleware);
-
-        $this->loadDependencies();
-        $this->afterBoot();
-    }
-
-    private function initEnv(): void
-    {
-        (new DotEnv($this->getEnvFile()))->load();
-        foreach (['APP_ENV' => self::DEFAULT_ENV, 'APP_TIMEZONE' => 'UTC', 'APP_LOCALE' => 'en', 'APP_DEBUG' => false] as $k => $value) {
-            if (getenv($k) === false) {
-                self::putEnv($k, $value);
-            }
-        }
-
-        $environments = self::getAvailableEnvironments();
-        if (!in_array(getenv('APP_ENV'), $environments)) {
-            throw new InvalidArgumentException(sprintf(
-                    'The env "%s" do not exist. Defined environments are: "%s".',
-                    getenv('APP_ENV'),
-                    implode('", "', $environments))
-            );
-        }
-        $this->env =  strtolower($_ENV['APP_ENV']);
-        $this->debug = $_ENV['APP_DEBUG'] ?: ($this->env === 'dev');
-    }
-
-    private function configureErrorHandling(): void
-    {
-        ini_set("log_errors", '1');
-        ini_set("error_log", filepath_join($this->getLogDir(), sprintf('%s_error.log', $this->getEnv())));
-
-        if ($this->getEnv() === 'dev') {
-            ErrorHandler::register();
-            return;
-        }
-
-        ini_set("display_startup_errors", '0');
-        ini_set("display_errors", '0');
-        ini_set("html_errors", '0');
-
-        error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
-    }
-
-    private function configureTimezone(): void
-    {
-        $timezone = getenv('APP_TIMEZONE');
-        if ($timezone === false) {
-            throw new \RuntimeException('APP_TIMEZONE environment variable is not set.');
-        }
-        date_default_timezone_set($timezone);
-    }
-
-    final public function loadConfigurationIfExists(string $fileName): array
-    {
-        $filePath = filepath_join( $this->getConfigDir(), $fileName);
-        if (file_exists($filePath)) {
-            return require $filePath;
-        }
-
-        return [];
-    }
-
-    private function loadDependencies(): void
-    {
-        list($services, $parameters, $listeners, $routes, $commands, $packages, $controllers) = $this->loadDependenciesConfiguration();
-        $definitions = array_merge(
-            $parameters,
-            $services,
-            [
-                'michel.packages' => $packages,
-                'michel.commands' => $commands,
-                'michel.listeners' => $listeners,
-                'michel.middleware' => $this->middlewareCollection,
-                BaseKernel::class => $this
-            ]
-        );
-        $definitions['michel.services_ids'] = array_keys($definitions);
-        $definitions['michel.controllers'] = static function (ContainerInterface $container) use ($controllers) {
-            $scanner = new ControllerFinder($controllers, $container->get('michel.current_cache'));
-            return $scanner->findControllerClasses();
-        };
-        $definitions['michel.routes'] = static function (ContainerInterface $container) use ($routes) {
-            $collector = null;
-            if (PHP_VERSION_ID >= 80000) {
-                $controllers = $container->get('michel.controllers');
-                $collector = new AttributeRouteCollector(
-                    $controllers,
-                    $container->get('michel.current_cache')
-                );
-            }
-            return array_merge($routes, $collector ? $collector->collect() : []);
-        };
-
-        $this->container = $this->loadContainer($definitions);
-        $this->debugDataCollector = $this->container->get(DebugDataCollector::class);
-        unset($services, $parameters, $listeners, $routes, $commands, $packages, $controllers, $definitions);
-    }
-
-    private function loadDependenciesConfiguration(): array
-    {
-        $services = $this->loadConfigurationIfExists('services.php');
-        $parameters = $this->loadParameters();
-        $listeners = $this->loadConfigurationIfExists('listeners.php');
-        $routes = $this->loadConfigurationIfExists('routes.php');
-        $commands = $this->loadConfigurationIfExists('commands.php');
-        $controllers = $this->loadConfigurationIfExists('controllers.php');
-        $packages = $this->getPackages();
-        foreach ($packages as $package) {
-            $services = array_merge($package->getDefinitions(), $services);
-            $parameters = array_merge($package->getParameters(), $parameters);
-            $listeners = array_merge_recursive($package->getListeners(), $listeners);
-            $routes = array_merge($package->getRoutes(), $routes);
-            $commands = array_merge($package->getCommandSources(), $commands);
-            $controllers = array_merge($package->getControllerSources(), $controllers);
-        }
-
-        return [$services, $parameters, $listeners, $routes, $commands, $packages, $controllers];
-    }
-
-    /**
-     * @return array<PackageInterface>
-     */
-    private function getPackages(): array
-    {
-        $packagesName = $this->loadConfigurationIfExists('packages.php');
-        $packages = [];
-        foreach ($packagesName as $packageName => $envs) {
-            if (!in_array($this->getEnv(), $envs)) {
-                continue;
-            }
-            $packages[] = new $packageName();
-        }
-        return $packages;
-    }
-
-    private function loadParameters(): array
-    {
-        $parameters = $this->loadConfigurationIfExists('parameters.php');
-        $parameters['michel.environment'] = $this->getEnv();
-        $parameters['michel.debug'] = $this->isDebug();
-        $parameters['michel.project_dir'] = $this->getProjectDir();
-        $parameters['michel.cache_dir'] = $this->getCacheDir();
-        $parameters['michel.logs_dir'] = $this->getLogDir();
-        $parameters['michel.config_dir'] = $this->getConfigDir();
-        $parameters['michel.public_dir'] = $this->getPublicDir();
-        $parameters['michel.current_cache'] = $this->getEnv() === 'dev' ? null : $this->getCacheDir();
-
-        return $parameters;
-    }
-
-    private static function getAvailableEnvironments(): array
-    {
-        return array_unique(array_merge(self::DEFAULT_ENVIRONMENTS, App::getCustomEnvironments()));
-    }
-
-    private static function putEnv(string $name, $value): void
-    {
-        putenv(sprintf('%s=%s', $name, is_bool($value) ? ($value ? '1' : '0') : $value));
-        $_ENV[$name] = $value;
-        $_SERVER[$name] = $value;
-    }
-}
+<?php
+declare(strict_types=1);
+
+namespace Michel\Framework\Core;
+
+use DateTimeImmutable;
+use Michel\Attribute\AttributeRouteCollector;
+use Michel\Env\DotEnv;
+use Michel\Framework\Core\Debug\DebugDataCollector;
+use Michel\Framework\Core\ErrorHandler\ErrorHandler;
+use Michel\Framework\Core\ErrorHandler\ExceptionHandler;
+use Michel\Framework\Core\Handler\RequestHandler;
+use Michel\Framework\Core\Http\Exception\HttpException;
+use Michel\Framework\Core\Http\Exception\HttpExceptionInterface;
+use InvalidArgumentException;
+use Michel\Framework\Core\Finder\ControllerFinder;
+use Michel\Framework\Core\Http\RequestContext;
+use Michel\Package\PackageInterface;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Throwable;
+use function array_filter;
+use function array_keys;
+use function array_merge;
+use function date_default_timezone_set;
+use function error_reporting;
+use function getenv;
+use function implode;
+use function in_array;
+use function json_encode;
+use function sprintf;
+
+/**
+ * @package    Michel.F
+ * @author    Michel 
+ * @license    https://opensource.org/license/mpl-2-0 Mozilla Public License v2.0
+ */
+abstract class BaseKernel
+{
+    private const DEFAULT_ENV = 'prod';
+    public const VERSION = '0.0.1-alpha';
+    public const NAME = 'MICHEL';
+    private const DEFAULT_ENVIRONMENTS = [
+        'dev',
+        'prod'
+    ];
+    private string $env = self::DEFAULT_ENV;
+    private bool $debug = false;
+
+    protected ContainerInterface $container;
+    /**
+     * @var array<MiddlewareInterface>|array<string>
+     */
+    private array $middlewareCollection = [];
+    private ?DebugDataCollector $debugDataCollector = null;
+
+    /**
+     * BaseKernel constructor.
+     */
+    public function __construct()
+    {
+        App::init($this->loadConfigurationIfExists('framework.php'));
+        $this->boot();
+    }
+
+    /**
+     * @param ServerRequestInterface $request
+     * @return ResponseInterface
+     * @throws Throwable
+     */
+    final public function handle(ServerRequestInterface $request): ResponseInterface
+    {
+        try {
+            $request = $request->withAttribute('request_id', strtoupper(uniqid('REQ')));
+            $request = $request->withAttribute('debug_collector', $this->debugDataCollector);
+
+            /**
+             * @var RequestContext $context
+             */
+            $context = $this->container->get(RequestContext::class);
+            $context->setRequest($request);
+
+            $requestHandler = new RequestHandler($this->container, $this->middlewareCollection);
+            $response =  $requestHandler->handle($request);
+            if ($response->getStatusCode() >= 400 && $response->getStatusCode() < 600) {
+                throw new HttpException($response->getStatusCode(), $response->getReasonPhrase());
+            }
+            return $response;
+        } catch (Throwable $exception) {
+            if (!$exception instanceof HttpExceptionInterface) {
+                $this->logException($exception, $request);
+            }
+
+            $exceptionHandler = $this->container->get(ExceptionHandler::class);
+            return $exceptionHandler->render($request, $exception);
+        }
+    }
+
+    final public function getEnv(): string
+    {
+        return $this->env;
+    }
+
+    final public function isDebug(): bool
+    {
+        return $this->debug;
+    }
+
+    final public function getContainer(): ContainerInterface
+    {
+        return $this->container;
+    }
+
+    abstract public function getProjectDir(): string;
+
+    abstract public function getCacheDir(): string;
+
+    abstract public function getLogDir(): string;
+
+    abstract public function getConfigDir(): string;
+
+    abstract public function getPublicDir(): string;
+
+    abstract public function getEnvFile(): string;
+
+    abstract protected function afterBoot(): void;
+
+    protected function loadContainer(array $definitions): ContainerInterface
+    {
+        return App::createContainer($definitions, ['cache_dir' => $this->getCacheDir()]);
+    }
+
+    final protected function logException(Throwable $exception, ServerRequestInterface $request): void
+    {
+        $this->log([
+            '@timestamp' => (new DateTimeImmutable())->format('c'),
+            'log.level' => 'error',
+            'id' => $request->getAttribute('request_id'),
+            'message' => $exception->getMessage(),
+            'http.request' => [
+                'method' => $request->getMethod(),
+                'url' => $request->getUri()->__toString(),
+            ],
+            'error' => [
+                'code' => $exception->getCode(),
+                'stack_trace' => $exception->getTrace(),
+                'class' => get_class($exception),
+            ],
+            'source' => [
+                'file' => $exception->getFile(),
+                'line' => $exception->getLine(),
+            ],
+        ], sprintf('%s_request_error.log', $this->getEnv()));
+    }
+
+    final protected function log(array $data, string $logFile = null): void
+    {
+        $logDir = $this->getLogDir();
+        if (empty($logDir)) {
+            throw new InvalidArgumentException('The log dir is empty, please set it in the Kernel.');
+        }
+
+        if (!is_dir($logDir) && !mkdir($logDir, 0777, true) && !is_dir($logDir)) {
+            throw new \RuntimeException(sprintf('Directory "%s" was not created', $logDir));
+        }
+        if ($logFile === null) {
+            $logFile = $this->getEnv() . '.log';
+        }
+        error_log(
+            json_encode($data, JSON_UNESCAPED_SLASHES) . PHP_EOL,
+            3,
+            filepath_join( $logDir, $logFile)
+        );
+    }
+
+    private function boot(): void
+    {
+        $this->initEnv();
+        $this->configureErrorHandling();
+        $this->configureTimezone();
+
+        $middleware = $this->loadConfigurationIfExists('middleware.php');
+        $middleware = array_filter($middleware, function ($environments) {
+            return in_array($this->getEnv(), $environments);
+        });
+        $this->middlewareCollection = array_keys($middleware);
+
+        $this->loadDependencies();
+        $this->afterBoot();
+    }
+
+    private function initEnv(): void
+    {
+        (new DotEnv($this->getEnvFile()))->load();
+        foreach (['APP_ENV' => self::DEFAULT_ENV, 'APP_TIMEZONE' => 'UTC', 'APP_LOCALE' => 'en', 'APP_DEBUG' => false] as $k => $value) {
+            if (getenv($k) === false) {
+                self::putEnv($k, $value);
+            }
+        }
+
+        $environments = self::getAvailableEnvironments();
+        if (!in_array(getenv('APP_ENV'), $environments)) {
+            throw new InvalidArgumentException(sprintf(
+                    'The env "%s" do not exist. Defined environments are: "%s".',
+                    getenv('APP_ENV'),
+                    implode('", "', $environments))
+            );
+        }
+        $this->env =  strtolower($_ENV['APP_ENV']);
+        $this->debug = $_ENV['APP_DEBUG'] ?: ($this->env === 'dev');
+    }
+
+    private function configureErrorHandling(): void
+    {
+        ini_set("log_errors", '1');
+        ini_set("error_log", filepath_join($this->getLogDir(), sprintf('%s_error.log', $this->getEnv())));
+
+        if ($this->getEnv() === 'dev') {
+            ErrorHandler::register();
+            return;
+        }
+
+        ini_set("display_startup_errors", '0');
+        ini_set("display_errors", '0');
+        ini_set("html_errors", '0');
+
+        error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
+    }
+
+    private function configureTimezone(): void
+    {
+        $timezone = getenv('APP_TIMEZONE');
+        if ($timezone === false) {
+            throw new \RuntimeException('APP_TIMEZONE environment variable is not set.');
+        }
+        date_default_timezone_set($timezone);
+    }
+
+    final public function loadConfigurationIfExists(string $fileName): array
+    {
+        $filePath = filepath_join( $this->getConfigDir(), $fileName);
+        if (file_exists($filePath)) {
+            return require $filePath;
+        }
+
+        return [];
+    }
+
+    private function loadDependencies(): void
+    {
+        list($services, $parameters, $listeners, $routes, $commands, $packages, $controllers) = $this->loadDependenciesConfiguration();
+        $definitions = array_merge(
+            $parameters,
+            $services,
+            [
+                'michel.packages' => $packages,
+                'michel.commands' => $commands,
+                'michel.listeners' => $listeners,
+                'michel.middleware' => $this->middlewareCollection,
+                BaseKernel::class => $this
+            ]
+        );
+        $definitions['michel.services_ids'] = array_keys($definitions);
+        $definitions['michel.controllers'] = static function (ContainerInterface $container) use ($controllers) {
+            $scanner = new ControllerFinder($controllers, $container->get('michel.current_cache'));
+            return $scanner->findControllerClasses();
+        };
+        $definitions['michel.routes'] = static function (ContainerInterface $container) use ($routes) {
+            $collector = null;
+            if (PHP_VERSION_ID >= 80000) {
+                $controllers = $container->get('michel.controllers');
+                $collector = new AttributeRouteCollector(
+                    $controllers,
+                    $container->get('michel.current_cache')
+                );
+            }
+            return array_merge($routes, $collector ? $collector->collect() : []);
+        };
+
+        $this->container = $this->loadContainer($definitions);
+        $this->debugDataCollector = $this->container->get(DebugDataCollector::class);
+        unset($services, $parameters, $listeners, $routes, $commands, $packages, $controllers, $definitions);
+    }
+
+    private function loadDependenciesConfiguration(): array
+    {
+        $services = $this->loadConfigurationIfExists('services.php');
+        $parameters = $this->loadParameters();
+        $listeners = $this->loadConfigurationIfExists('listeners.php');
+        $routes = $this->loadConfigurationIfExists('routes.php');
+        $commands = $this->loadConfigurationIfExists('commands.php');
+        $controllers = $this->loadConfigurationIfExists('controllers.php');
+        $packages = $this->getPackages();
+        foreach ($packages as $package) {
+            $services = array_merge($package->getDefinitions(), $services);
+            $parameters = array_merge($package->getParameters(), $parameters);
+            $listeners = array_merge_recursive($package->getListeners(), $listeners);
+            $routes = array_merge($package->getRoutes(), $routes);
+            $commands = array_merge($package->getCommandSources(), $commands);
+            $controllers = array_merge($package->getControllerSources(), $controllers);
+        }
+
+        return [$services, $parameters, $listeners, $routes, $commands, $packages, $controllers];
+    }
+
+    /**
+     * @return array<PackageInterface>
+     */
+    private function getPackages(): array
+    {
+        $packagesName = $this->loadConfigurationIfExists('packages.php');
+        $packages = [];
+        foreach ($packagesName as $packageName => $envs) {
+            if (!in_array($this->getEnv(), $envs)) {
+                continue;
+            }
+            $packages[] = new $packageName();
+        }
+        return $packages;
+    }
+
+    private function loadParameters(): array
+    {
+        $parameters = $this->loadConfigurationIfExists('parameters.php');
+        $parameters['michel.environment'] = $this->getEnv();
+        $parameters['michel.debug'] = $this->isDebug();
+        $parameters['michel.project_dir'] = $this->getProjectDir();
+        $parameters['michel.cache_dir'] = $this->getCacheDir();
+        $parameters['michel.logs_dir'] = $this->getLogDir();
+        $parameters['michel.config_dir'] = $this->getConfigDir();
+        $parameters['michel.public_dir'] = $this->getPublicDir();
+        $parameters['michel.current_cache'] = $this->getEnv() === 'dev' ? null : $this->getCacheDir();
+
+        return $parameters;
+    }
+
+    private static function getAvailableEnvironments(): array
+    {
+        return array_unique(array_merge(self::DEFAULT_ENVIRONMENTS, App::getCustomEnvironments()));
+    }
+
+    private static function putEnv(string $name, $value): void
+    {
+        putenv(sprintf('%s=%s', $name, is_bool($value) ? ($value ? '1' : '0') : $value));
+        $_ENV[$name] = $value;
+        $_SERVER[$name] = $value;
+    }
+}

+ 211 - 211
src/Package/MichelCorePackage.php

@@ -1,211 +1,211 @@
-<?php
-
-namespace Michel\Framework\Core\Package;
-
-use LogicException;
-use Michel\Console\CommandRunner;
-use Michel\EventDispatcher\EventDispatcher;
-use Michel\EventDispatcher\ListenerProvider;
-use Michel\Framework\Core\Command\CacheClearCommand;
-use Michel\Framework\Core\Command\DebugContainerCommand;
-use Michel\Framework\Core\Command\DebugEnvCommand;
-use Michel\Framework\Core\Command\DebugRouteCommand;
-use Michel\Framework\Core\Command\LogClearCommand;
-use Michel\Framework\Core\Command\MakeCommandCommand;
-use Michel\Framework\Core\Command\MakeControllerCommand;
-use Michel\Framework\Core\Config\ConfigProvider;
-use Michel\Framework\Core\Debug\DebugDataCollector;
-use Michel\Framework\Core\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
-use Michel\Framework\Core\ErrorHandler\ExceptionHandler;
-use Michel\Framework\Core\Http\RequestContext;
-use Michel\Framework\Core\Middlewares\DebugMiddleware;
-use Michel\Framework\Core\Middlewares\ForceHttpsMiddleware;
-use Michel\Framework\Core\Middlewares\IpRestrictionMiddleware;
-use Michel\Framework\Core\Middlewares\MaintenanceMiddleware;
-use Michel\Package\PackageInterface;
-use Michel\PurePlate\Engine;
-use Michel\Route;
-use Michel\Router;
-use Michel\RouterInterface;
-use Michel\RouterMiddleware;
-use Psr\Container\ContainerInterface;
-use Psr\EventDispatcher\EventDispatcherInterface;
-use function getenv;
-
-final class MichelCorePackage implements PackageInterface
-{
-    public function getDefinitions(): array
-    {
-        return [
-            RequestContext::class => static function (ContainerInterface $container): RequestContext {
-                return new RequestContext();
-            },
-            ConfigProvider::class => static function (ContainerInterface $container): ConfigProvider {
-                return new ConfigProvider($container);
-            },
-            EventDispatcherInterface::class => static function (ContainerInterface $container): EventDispatcherInterface {
-                $events = $container->get('michel.listeners');
-                $provider = new ListenerProvider();
-                foreach ($events as $event => $listeners) {
-                    if (is_array($listeners)) {
-                        foreach ($listeners as $listener) {
-                            $provider->addListener($event, $container->get($listener));
-                        }
-                    } elseif (is_object($listeners)) {
-                        $provider->addListener($event, $listeners);
-                    } else {
-                        $provider->addListener($event, $container->get($listeners));
-                    }
-                }
-                unset($events);
-                return new EventDispatcher($provider);
-            },
-            CommandRunner::class => static function (ContainerInterface $container): CommandRunner {
-                $commandList = $container->get('michel.commands');
-                $commands = [];
-                foreach ($commandList as $commandName) {
-                    $commands[] = $container->get($commandName);
-                }
-                unset($commandList);
-                return new CommandRunner($commands);
-            },
-            'render' => static function (ContainerInterface $container) {
-                return $container->get(Engine::class);
-            },
-            Engine::class => static function (ContainerInterface $container) {
-                return new Engine(
-                    $container->get('app.template_dir'),
-                    $container->get('michel.environment') === 'dev',
-                    filepath_join($container->get('michel.cache_dir'), 'pure'),
-                    [
-                        '_container' => $container,
-                        '_env' => $container->get('michel.environment'),
-                        '_debug' => $container->get('michel.debug'),
-                        '_context' => $container->get(RequestContext::class)
-                    ]
-                );
-            },
-            RouterInterface::class => static function (ContainerInterface $container): object {
-                /**
-                 * @var array<Route> $routes
-                 */
-                $routes = $container->get('michel.routes');
-                $router = new Router($routes, $container->get('app.url'));
-                unset($routes);
-                return $router;
-            },
-            DebugDataCollector::class => static function (ContainerInterface $container): DebugDataCollector {
-                return new DebugDataCollector($container->get('michel.debug'));
-            },
-            DebugMiddleware::class => static function (ContainerInterface $container) {
-                return new DebugMiddleware([
-                    'debug' => $container->get('michel.debug'),
-                    'profiler' => $container->get('app.profiler'),
-                    'env' => $container->get('michel.environment'),
-                    'log_dir' => $container->get('michel.logs_dir'),
-                ]);
-            },
-            RouterMiddleware::class => static function (ContainerInterface $container) {
-                return new RouterMiddleware($container->get(RouterInterface::class), response_factory());
-            },
-            ForceHttpsMiddleware::class => static function (ContainerInterface $container) {
-                /**
-                 * @var ConfigProvider $configProvider
-                 */
-                $configProvider = $container->get(ConfigProvider::class);
-                return new ForceHttpsMiddleware($configProvider->isForceHttps(), response_factory());
-            },
-            IpRestrictionMiddleware::class => static function (ContainerInterface $container) {
-                /**
-                 * @var ConfigProvider $configProvider
-                 */
-                $configProvider = $container->get(ConfigProvider::class);
-                return new IpRestrictionMiddleware($configProvider->getAllowedIps(), response_factory());
-            },
-            MaintenanceMiddleware::class => static function (ContainerInterface $container) {
-                /**
-                 * @var ConfigProvider $configProvider
-                 */
-                $configProvider = $container->get(ConfigProvider::class);
-                return new MaintenanceMiddleware(
-                    $configProvider->isMaintenance(),
-                    response_factory(),
-                    $configProvider->getAllowedIps()
-                );
-            },
-            ExceptionHandler::class => static function (ContainerInterface $container) {
-                /**
-                 * @var ConfigProvider $configProvider
-                 */
-                $configProvider = $container->get(ConfigProvider::class);
-
-                return new ExceptionHandler(response_factory(), [
-                        'debug' => $container->get('michel.debug'),
-                        'html_response' => new HtmlErrorRenderer(
-                            response_factory(),
-                            $container->get('michel.debug'),
-                            filepath_join($configProvider->getTemplateDir(), '_exception')
-                        )
-                    ]
-                );
-            }
-        ];
-    }
-
-    public function getParameters(): array
-    {
-        return [
-            'app.url' => getenv('APP_URL') ?: '', // Application URL
-            'app.locale' => getenv('APP_LOCALE') ?: 'en', // Default locale
-            'app.template_dir' => getenv('APP_TEMPLATE_DIR') ?: function (ContainerInterface $container) {
-                return filepath_join($container->get('michel.project_dir'), 'templates');
-            }, // Template directory
-            'app.allowed_ips' => getenv('APP_ALLOWED_IPS') ?: '', // Allowed IP addresses
-            'app.secret_key' => getenv('APP_SECRET_KEY') ?: '', // Secret
-            'app.maintenance' => $_ENV['APP_MAINTENANCE'] ?? false, // Maintenance mode
-            'app.force_https' => $_ENV['APP_FORCE_HTTPS'] ?? false, // Force HTTPS
-            'app.profiler' => $_ENV['APP_PROFILER'] ?? function (ContainerInterface $container) {
-                    return $container->get('michel.environment') == 'dev';
-                }, // Debug mode
-        ];
-    }
-
-    public function getRoutes(): array
-    {
-        return [];
-    }
-
-    public function getListeners(): array
-    {
-        return [];
-    }
-
-    /**
-     * Return an array of controller sources to scan for attribute-based routes.
-     *
-     * Each source can be either:
-     * - A fully-qualified class name (FQCN), e.g. App\Controller\PingController::class
-     * - A directory path (string), e.g. __DIR__ . '/../src/Controller'
-     *
-     * This allows the router to scan specific controllers or entire folders.
-     *
-     * @return string[] Array of class names and/or absolute folder paths.
-     */
-    public function getControllerSources(): array
-    {
-        return [];
-    }
-
-    public function getCommandSources(): array
-    {
-        return [
-            CacheClearCommand::class,
-            LogClearCommand::class,
-            MakeControllerCommand::class,
-            MakeCommandCommand::class,
-            DebugEnvCommand::class,
-            DebugContainerCommand::class,
-            DebugRouteCommand::class,
-        ];
-    }
-}
+<?php
+
+namespace Michel\Framework\Core\Package;
+
+use LogicException;
+use Michel\Console\CommandRunner;
+use Michel\EventDispatcher\EventDispatcher;
+use Michel\EventDispatcher\ListenerProvider;
+use Michel\Framework\Core\Command\CacheClearCommand;
+use Michel\Framework\Core\Command\DebugContainerCommand;
+use Michel\Framework\Core\Command\DebugEnvCommand;
+use Michel\Framework\Core\Command\DebugRouteCommand;
+use Michel\Framework\Core\Command\LogClearCommand;
+use Michel\Framework\Core\Command\MakeCommandCommand;
+use Michel\Framework\Core\Command\MakeControllerCommand;
+use Michel\Framework\Core\Config\ConfigProvider;
+use Michel\Framework\Core\Debug\DebugDataCollector;
+use Michel\Framework\Core\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
+use Michel\Framework\Core\ErrorHandler\ExceptionHandler;
+use Michel\Framework\Core\Http\RequestContext;
+use Michel\Framework\Core\Middlewares\DebugMiddleware;
+use Michel\Framework\Core\Middlewares\ForceHttpsMiddleware;
+use Michel\Framework\Core\Middlewares\IpRestrictionMiddleware;
+use Michel\Framework\Core\Middlewares\MaintenanceMiddleware;
+use Michel\Package\PackageInterface;
+use Michel\PurePlate\Engine;
+use Michel\Route;
+use Michel\Router;
+use Michel\RouterInterface;
+use Michel\RouterMiddleware;
+use Psr\Container\ContainerInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
+use function getenv;
+
+final class MichelCorePackage implements PackageInterface
+{
+    public function getDefinitions(): array
+    {
+        return [
+            RequestContext::class => static function (ContainerInterface $container): RequestContext {
+                return new RequestContext();
+            },
+            ConfigProvider::class => static function (ContainerInterface $container): ConfigProvider {
+                return new ConfigProvider($container);
+            },
+            EventDispatcherInterface::class => static function (ContainerInterface $container): EventDispatcherInterface {
+                $events = $container->get('michel.listeners');
+                $provider = new ListenerProvider();
+                foreach ($events as $event => $listeners) {
+                    if (is_array($listeners)) {
+                        foreach ($listeners as $listener) {
+                            $provider->addListener($event, $container->get($listener));
+                        }
+                    } elseif (is_object($listeners)) {
+                        $provider->addListener($event, $listeners);
+                    } else {
+                        $provider->addListener($event, $container->get($listeners));
+                    }
+                }
+                unset($events);
+                return new EventDispatcher($provider);
+            },
+            CommandRunner::class => static function (ContainerInterface $container): CommandRunner {
+                $commandList = $container->get('michel.commands');
+                $commands = [];
+                foreach ($commandList as $commandName) {
+                    $commands[] = $container->get($commandName);
+                }
+                unset($commandList);
+                return new CommandRunner($commands);
+            },
+            'render' => static function (ContainerInterface $container) {
+                return $container->get(Engine::class);
+            },
+            Engine::class => static function (ContainerInterface $container) {
+                return new Engine(
+                    $container->get('app.template_dir'),
+                    $container->get('michel.environment') === 'dev',
+                    filepath_join($container->get('michel.cache_dir'), 'plate'),
+                    [
+                        '_container' => $container,
+                        '_env' => $container->get('michel.environment'),
+                        '_debug' => $container->get('michel.debug'),
+                        '_context' => $container->get(RequestContext::class)
+                    ]
+                );
+            },
+            RouterInterface::class => static function (ContainerInterface $container): object {
+                /**
+                 * @var array<Route> $routes
+                 */
+                $routes = $container->get('michel.routes');
+                $router = new Router($routes, $container->get('app.url'));
+                unset($routes);
+                return $router;
+            },
+            DebugDataCollector::class => static function (ContainerInterface $container): DebugDataCollector {
+                return new DebugDataCollector($container->get('michel.debug'));
+            },
+            DebugMiddleware::class => static function (ContainerInterface $container) {
+                return new DebugMiddleware([
+                    'debug' => $container->get('michel.debug'),
+                    'profiler' => $container->get('app.profiler'),
+                    'env' => $container->get('michel.environment'),
+                    'log_dir' => $container->get('michel.logs_dir'),
+                ]);
+            },
+            RouterMiddleware::class => static function (ContainerInterface $container) {
+                return new RouterMiddleware($container->get(RouterInterface::class), response_factory());
+            },
+            ForceHttpsMiddleware::class => static function (ContainerInterface $container) {
+                /**
+                 * @var ConfigProvider $configProvider
+                 */
+                $configProvider = $container->get(ConfigProvider::class);
+                return new ForceHttpsMiddleware($configProvider->isForceHttps(), response_factory());
+            },
+            IpRestrictionMiddleware::class => static function (ContainerInterface $container) {
+                /**
+                 * @var ConfigProvider $configProvider
+                 */
+                $configProvider = $container->get(ConfigProvider::class);
+                return new IpRestrictionMiddleware($configProvider->getAllowedIps(), response_factory());
+            },
+            MaintenanceMiddleware::class => static function (ContainerInterface $container) {
+                /**
+                 * @var ConfigProvider $configProvider
+                 */
+                $configProvider = $container->get(ConfigProvider::class);
+                return new MaintenanceMiddleware(
+                    $configProvider->isMaintenance(),
+                    response_factory(),
+                    $configProvider->getAllowedIps()
+                );
+            },
+            ExceptionHandler::class => static function (ContainerInterface $container) {
+                /**
+                 * @var ConfigProvider $configProvider
+                 */
+                $configProvider = $container->get(ConfigProvider::class);
+
+                return new ExceptionHandler(response_factory(), [
+                        'debug' => $container->get('michel.debug'),
+                        'html_response' => new HtmlErrorRenderer(
+                            response_factory(),
+                            $container->get('michel.debug'),
+                            filepath_join($configProvider->getTemplateDir(), '_exception')
+                        )
+                    ]
+                );
+            }
+        ];
+    }
+
+    public function getParameters(): array
+    {
+        return [
+            'app.url' => getenv('APP_URL') ?: '', // Application URL
+            'app.locale' => getenv('APP_LOCALE') ?: 'en', // Default locale
+            'app.template_dir' => getenv('APP_TEMPLATE_DIR') ?: function (ContainerInterface $container) {
+                return filepath_join($container->get('michel.project_dir'), 'templates');
+            }, // Template directory
+            'app.allowed_ips' => getenv('APP_ALLOWED_IPS') ?: '', // Allowed IP addresses
+            'app.secret_key' => getenv('APP_SECRET_KEY') ?: '', // Secret
+            'app.maintenance' => $_ENV['APP_MAINTENANCE'] ?? false, // Maintenance mode
+            'app.force_https' => $_ENV['APP_FORCE_HTTPS'] ?? false, // Force HTTPS
+            'app.profiler' => $_ENV['APP_PROFILER'] ?? function (ContainerInterface $container) {
+                    return $container->get('michel.environment') == 'dev';
+                }, // Debug mode
+        ];
+    }
+
+    public function getRoutes(): array
+    {
+        return [];
+    }
+
+    public function getListeners(): array
+    {
+        return [];
+    }
+
+    /**
+     * Return an array of controller sources to scan for attribute-based routes.
+     *
+     * Each source can be either:
+     * - A fully-qualified class name (FQCN), e.g. App\Controller\PingController::class
+     * - A directory path (string), e.g. __DIR__ . '/../src/Controller'
+     *
+     * This allows the router to scan specific controllers or entire folders.
+     *
+     * @return string[] Array of class names and/or absolute folder paths.
+     */
+    public function getControllerSources(): array
+    {
+        return [];
+    }
+
+    public function getCommandSources(): array
+    {
+        return [
+            CacheClearCommand::class,
+            LogClearCommand::class,
+            MakeControllerCommand::class,
+            MakeCommandCommand::class,
+            DebugEnvCommand::class,
+            DebugContainerCommand::class,
+            DebugRouteCommand::class,
+        ];
+    }
+}