瀏覽代碼

Initial commit for the Michel core framework structure.

michelphp 17 小時之前
當前提交
c9b94cc297
共有 74 個文件被更改,包括 5147 次插入0 次删除
  1. 4 0
      .gitignore
  2. 373 0
      LICENSE
  3. 187 0
      README.md
  4. 47 0
      composer.json
  5. 216 0
      functions/helpers.php
  6. 70 0
      functions/helpers_array.php
  7. 49 0
      functions/helpers_date.php
  8. 28 0
      functions/helpers_file.php
  9. 64 0
      functions/helpers_string.php
  10. 243 0
      resources/debug/debugbar.html.php
  11. 28 0
      resources/views/error.html.php
  12. 111 0
      src/App.php
  13. 285 0
      src/BaseKernel.php
  14. 43 0
      src/Command/AbstractMakeCommand.php
  15. 76 0
      src/Command/CacheClearCommand.php
  16. 86 0
      src/Command/DebugContainerCommand.php
  17. 53 0
      src/Command/DebugEnvCommand.php
  18. 70 0
      src/Command/DebugRouteCommand.php
  19. 76 0
      src/Command/LogClearCommand.php
  20. 91 0
      src/Command/MakeCommandCommand.php
  21. 68 0
      src/Command/MakeControllerCommand.php
  22. 73 0
      src/Config/ConfigProvider.php
  23. 48 0
      src/Controller/Controller.php
  24. 35 0
      src/Debug/DebugDataCollector.php
  25. 60 0
      src/Debug/ExecutionProfiler.php
  26. 88 0
      src/Debug/RequestProfiler.php
  27. 73 0
      src/Dependency.php
  28. 52 0
      src/ErrorHandler/ErrorHandler.php
  29. 88 0
      src/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php
  30. 45 0
      src/ErrorHandler/ErrorRenderer/JsonErrorRenderer.php
  31. 63 0
      src/ErrorHandler/ExceptionHandler.php
  32. 69 0
      src/Handler/RequestHandler.php
  33. 27 0
      src/Helper/IpHelper.php
  34. 18 0
      src/Http/Exception/BadRequestException.php
  35. 18 0
      src/Http/Exception/ForbiddenException.php
  36. 37 0
      src/Http/Exception/HttpException.php
  37. 25 0
      src/Http/Exception/HttpExceptionInterface.php
  38. 18 0
      src/Http/Exception/MethodNotAllowedException.php
  39. 18 0
      src/Http/Exception/NotFoundException.php
  40. 18 0
      src/Http/Exception/UnauthorizedException.php
  41. 123 0
      src/Middlewares/ControllerMiddleware.php
  42. 131 0
      src/Middlewares/DebugMiddleware.php
  43. 33 0
      src/Middlewares/ForceHttpsMiddleware.php
  44. 45 0
      src/Middlewares/IpRestrictionMiddleware.php
  45. 79 0
      src/Middlewares/MaintenanceMiddleware.php
  46. 206 0
      src/Package/MichelCorePackage.php
  47. 128 0
      src/Routing/ControllerFinder.php
  48. 4 0
      tests/.env
  49. 0 0
      tests/.env.test
  50. 2 0
      tests/.gitignore
  51. 75 0
      tests/AppTest.php
  52. 33 0
      tests/Controller/SampleControllerTest.php
  53. 24 0
      tests/Controller/UserControllerTest.php
  54. 65 0
      tests/ControllerFinderTest.php
  55. 78 0
      tests/ControllerMiddlewareTest.php
  56. 55 0
      tests/ControllerTest.php
  57. 41 0
      tests/ErrorHandlerTest.php
  58. 51 0
      tests/Kernel/SampleKernelTest.php
  59. 120 0
      tests/KernelTest.php
  60. 96 0
      tests/MichelCorePackageTest.php
  61. 18 0
      tests/Middleware/ResponseMiddlewareTest.php
  62. 29 0
      tests/Mock/ContainerMock.php
  63. 18 0
      tests/Mock/MiddlewareMock.php
  64. 17 0
      tests/Mock/RequestHandlerMock.php
  65. 82 0
      tests/Mock/ResponseMock.php
  66. 172 0
      tests/Mock/ServerRequestMock.php
  67. 63 0
      tests/Package/MyPackageTest.php
  68. 75 0
      tests/RequestHandlerTest.php
  69. 79 0
      tests/Response/ResponseTest.php
  70. 5 0
      tests/config/controllers.php
  71. 39 0
      tests/config/framework.php
  72. 6 0
      tests/config/middleware.php
  73. 7 0
      tests/config/packages.php
  74. 7 0
      tests/config/parameters.php

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+/vendor/
+/.vscode/
+/.idea/
+composer.lock

+ 373 - 0
LICENSE

@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.

+ 187 - 0
README.md

@@ -0,0 +1,187 @@
+# Michel Framework Core
+
+The Michel Framework Core is the core library of the Michel Framework, a lightweight and versatile PHP framework designed for simplifying web development tasks.
+
+## Installation
+
+You can install the Michel Framework Core via Composer:
+
+```bash
+composer require michel/michel-core
+```
+## Usage
+
+To use the Michel Framework Core, you can follow these steps:
+
+1. **Require the Composer Autoloader**: Include Composer's autoloader in your project's entry point (e.g., `index.php`).
+
+```php
+define('MICHEL_COMPOSER_AUTOLOAD_FILE', dirname(__DIR__) .'/vendor/autoload.php');
+require_once MICHEL_COMPOSER_AUTOLOAD_FILE;
+```
+
+2. **Create a .env File**
+
+Create a `.env` file in the directory defined by `getProjectDir` in your Kernel class. This file should contain essential environment configuration variables. At a minimum, include the following variables:
+```env
+APP_ENV=prod
+```
+Customize the `.env` file further with any additional environment-specific variables your application requires.
+
+3. **Configuration Files**:
+
+Before creating the kernel and booting up the Michel Framework, it's crucial to ensure that your project's `config` directory contains the necessary configuration files. These files define various aspects of your application, such as services, routes, middleware, and more.
+
+Here is a list of the essential configuration files that should be present in your `config` directory:
+
+- `commands.php`: This file defines custom artisan commands for your application.
+
+- `framework.php`: Framework-specific configuration, including options related to request handling, response generation, and other core settings.
+
+- `listeners.php`: Contains event listeners and their associated event-handling logic.
+
+- `middleware.php`: Lists middleware classes.
+
+- `packages.php`: Defines the packages or external dependencies your application relies on.
+
+- `parameters.php`: Stores parameters, often used to configure services or other parts of your application.
+
+- `routes.php`: Specifies the routes and associated controllers for your application's endpoints.
+
+- `services.php`: Contains definitions for services, their dependencies, and how they should be instantiated.
+
+Ensure that these configuration files are correctly populated and tailored to your project's requirements. Proper configuration is essential for the Michel Framework to function as expected and deliver the desired behavior.
+
+With the necessary configuration files in place, you can proceed with creating the kernel and launching your Michel Framework-powered application.
+ 
+4. **Create a Kernel Class**: Create a `Kernel` class that extends `BaseKernel` from the Michel Framework Core. This class is the heart of your application and is responsible for configuration and bootstrapping.
+```php
+final class Kernel extends BaseKernel
+{
+    public function getCacheDir(): string
+    {
+        return $this->getProjectDir() . '/var/cache';
+    }
+
+    public function getProjectDir(): string
+    {
+        return dirname(__DIR__);
+    }
+
+    public function getLogDir(): string
+    {
+        return $this->getProjectDir() . '/var/log';
+    }
+
+    public function getConfigDir(): string
+    {
+        return $this->getProjectDir() . '/config';
+    }
+
+    public function getPublicDir(): string
+    {
+        return $this->getProjectDir() . '/public';
+    }
+
+    protected function afterBoot(): void
+    {
+        // You can perform additional setup or actions here after the framework has booted.
+    }
+}
+
+```
+You can customize the `Kernel` class by implementing methods like `getCacheDir`, `getProjectDir`, `getLogDir`, `getConfigDir`, and `getPublicDir` to define the directories used by your application.
+
+5. **Configuration Array**: To configure the framework, you must define its settings using a configuration array. This array should be stored in a file named `framework.php` within the directory defined by `public function getConfigDir(): string` in the kernel.
+
+```php
+return [
+
+    // Framework Settings
+
+    'server_request' => static function (): ServerRequestInterface {
+        return ServerRequestFactory::fromGlobals();
+    },
+
+    'response_factory' => static function (): ResponseFactoryInterface {
+        return new ResponseFactory();
+    },
+
+    'container' => static function (array $definitions, array $options): ContainerInterface {
+
+        // Customize the container configuration here
+        
+        return new Container(
+            $definitions,
+            new ReflectionResolver()
+        );
+    },
+
+    'custom_environments' => [],
+
+];
+```
+
+6. **Initialize the Framework**: Instantiate the `Kernel` class and configure it to meet your application's requirements.
+
+```php
+$kernel = new Kernel();
+if (php_sapi_name() !== 'cli') {
+    $response = $kernel->handle(App::createServerRequest());
+    \send_http_response($response);
+}
+```
+# Michel Framework Core Configuration
+
+The configuration file for the Michel Framework Core, named `framework.php`, allows you to customize critical aspects of the framework's operation. It defines several key components and functions that are used by the framework to process HTTP requests and manage dependencies.
+
+## Server Request Configuration
+
+The `server_request` section is a pivotal aspect of configuring the Michel Framework Core for PSR-7 compliance. In order to use this section effectively, you must first install a PSR-7-compatible library of your choice, as the framework does not have a default PSR-7 implementation.
+
+Once you've installed a PSR-7 library, you can define a custom function within the `server_request` section to specify how the framework should create instances of the `ServerRequestInterface`. This customization allows you to tailor the request instantiation process to align with the PSR-7 specification and your application's specific requirements.
+
+Here's an example of how you might configure the `server_request` section to use the Laminas Diactoros library for request instantiation:
+
+```php
+'server_request' => static function (): ServerRequestInterface {
+    // Instantiate a ServerRequest using Laminas Diactoros or your preferred PSR-7 library.
+    return \Laminas\Diactoros\ServerRequestFactory::fromGlobals();
+},
+```
+
+## Response Factory Configuration
+
+The `response_factory` section is a critical component responsible for generating HTTP responses within the Michel Framework Core, and it operates in alignment with the PSR-17 standard (HTTP Factories). However, to use this section effectively, it's essential to install a PSR-17-compatible library of your choice since the framework does not include a default PSR-17 implementation.
+
+Once you've installed a PSR-17 library, you can define a custom function within the `response_factory` section to specify how the framework should create instances of the `ResponseFactoryInterface`. This customization allows you to tailor the response generation process to comply with the PSR-17 specification and your application's specific needs.
+
+Here's an example of how you might configure the `response_factory` section to use the Laminas Diactoros library for response instantiation:
+
+```php
+'response_factory' => static function (): ResponseFactoryInterface {
+    // Instantiate a ResponseFactory using Laminas Diactoros or your preferred PSR-17 library.
+    return new \Laminas\Diactoros\ResponseFactory();
+},
+```
+
+## Container Configuration
+
+The `container` section is integral to managing dependencies and services within the Michel Framework Core. To use this section effectively, it's essential to have a PSR-11 compatible container implementation installed, as the framework does not provide a default PSR-11 container.
+
+For example, you can configure the `container` section to use the `\Michel\DependencyInjection\Container` provided by the DevCoder library for dependency injection. Here's an example of how you might set it up:
+
+```php
+'container' => static function (array $definitions, array $options): ContainerInterface {
+    // Instantiate a PSR-11 compatible container using \Michel\DependencyInjection\Container or your preferred library.
+    return new \Michel\DependencyInjection\Container($definitions, new \Michel\DependencyInjection\ReflectionResolver());
+},
+```
+
+## Custom Environments
+
+The `custom_environments` section is an array that allows you to define custom application environments beyond the standard 'development' and 'production' environments. You can use these custom environments to handle different aspects of your application based on specific requirements.
+
+```php
+'custom_environments' => [],
+```

+ 47 - 0
composer.json

@@ -0,0 +1,47 @@
+{
+    "name": "michel/michel-core",
+    "description": "The Michel Framework Core provides the essential building blocks for the Michel Framework. It's a lightweight, versatile, and modern PHP framework designed to streamline web development and simplify common tasks.",
+    "type": "package",
+    "license": "MPL-2.0",
+    "authors": [
+        {
+            "name": "Michel.F"
+        }
+    ],
+    "autoload": {
+        "psr-4": {
+            "\\Michel\\Framework\\Core\\": "src",
+            "Test\\Michel\\Framework\\Core\\": "tests"
+        },
+        "files": [
+            "functions/helpers.php",
+            "functions/helpers_file.php",
+            "functions/helpers_string.php",
+            "functions/helpers_array.php",
+            "functions/helpers_date.php"
+        ]
+    },
+    "require": {
+        "php": ">=7.4",
+        "ext-json": "*",
+        "ext-mbstring": "*",
+        "psr/container": "^1.0|^2.0",
+        "psr/http-message": "~1.0|^2.0",
+        "psr/http-server-handler": "^1.0",
+        "psr/http-server-middleware": "^1.0",
+        "psr/event-dispatcher": "^1.0",
+        "psr/http-factory": "^1.0",
+        "psr/log": "^1.1|^2.0",
+        "michel/dotenv": "^1.0",
+        "michel/router": "^1.0",
+        "michel/options-resolver": "^1.0",
+        "michel/console": "^1.0",
+        "michel/vardumper": "^1.0",
+        "michel/psr14-event-dispatcher": "^1.0",
+        "michel/pure-plate": "^1.0",
+        "michel/michel-package-starter": "^1.0"
+    },
+    "require-dev": {
+      "michel/unitester": "^1.0.0"
+    }
+}

+ 216 - 0
functions/helpers.php

@@ -0,0 +1,216 @@
+<?php
+
+use Composer\Autoload\ClassLoader;
+use Michel\Framework\Core\App;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestFactoryInterface;
+use Psr\Http\Message\ServerRequestInterface;
+
+
+if (!function_exists('michel_composer_loader')) {
+
+    /**
+     * Returns the instance of the Composer class loader.
+     *
+     * @return ClassLoader
+     * @throws LogicException If the MICHEL_COMPOSER_AUTOLOAD_FILE constant is not defined.
+     */
+    function michel_composer_loader(): ClassLoader
+    {
+        if (!defined('MICHEL_COMPOSER_AUTOLOAD_FILE')) {
+            throw new LogicException('MICHEL_COMPOSER_AUTOLOAD_FILE const must be defined!');
+        }
+        return require MICHEL_COMPOSER_AUTOLOAD_FILE;
+    }
+}
+
+if (!function_exists('send_http_response')) {
+
+    /**
+     * Sends the HTTP response to the client.
+     *
+     * @param ResponseInterface $response The HTTP response to send.
+     */
+    function send_http_response(ResponseInterface $response)
+    {
+        $httpLine = sprintf('HTTP/%s %s %s',
+            $response->getProtocolVersion(),
+            $response->getStatusCode(),
+            $response->getReasonPhrase()
+        );
+
+        if (!headers_sent()) {
+            header($httpLine, true, $response->getStatusCode());
+
+            foreach ($response->getHeaders() as $name => $values) {
+                foreach ($values as $value) {
+                    header("$name: $value", false);
+                }
+            }
+        }
+
+        echo $response->getBody();
+    }
+}
+
+if (!function_exists('container')) {
+
+    /**
+     * Retrieves the application's dependency injection container.
+     *
+     * @return ContainerInterface The dependency injection container.
+     */
+    function container(): ContainerInterface
+    {
+        return App::getContainer();
+    }
+}
+
+if (!function_exists('create_request')) {
+
+    /**
+     * Creates a new HTTP request.
+     *
+     * @return ServerRequestInterface The HTTP response.
+     */
+    function create_request(): ServerRequestInterface
+    {
+        return App::createServerRequest();
+    }
+}
+
+if (!function_exists('request_factory')) {
+
+    /**
+     * Creates a new HTTP request.
+     *
+     * @return ServerRequestFactoryInterface The HTTP response.
+     */
+    function request_factory(): ServerRequestFactoryInterface
+    {
+        return App::getServerRequestFactory();
+    }
+}
+
+if (!function_exists('response_factory')) {
+
+    /**
+     * Retrieves the response factory.
+     *
+     * @return ResponseFactoryInterface The response factory.
+     */
+    function response_factory(): ResponseFactoryInterface
+    {
+        return App::getResponseFactory();
+    }
+}
+
+if (!function_exists('response')) {
+
+    /**
+     * Creates a new HTTP response.
+     *
+     * @param string $content The response content.
+     * @param int $status The HTTP status code.
+     * @return ResponseInterface The HTTP response.
+     */
+    function response(string $content = '', int $status = 200, $contentType = 'text/html'): ResponseInterface
+    {
+        $response = response_factory()->createResponse($status);
+        $response->getBody()->write($content);
+        return $response->withHeader('Content-Type', $contentType);
+    }
+}
+
+if (!function_exists('json_response')) {
+
+    /**
+     * Creates a new JSON response.
+     *
+     * @param array|JsonSerializable $data The data to encode to JSON.
+     * @param int $status The HTTP status code.
+     * @param int $flags JSON encoding flags.
+     * @return ResponseInterface The JSON response.
+     * @throws InvalidArgumentException If JSON encoding fails.
+     */
+    function json_response($data, int $status = 200, int $flags = JSON_HEX_TAG
+    | JSON_HEX_APOS
+    | JSON_HEX_AMP
+    | JSON_HEX_QUOT
+    | JSON_UNESCAPED_SLASHES): ResponseInterface
+    {
+        if (!is_array($data) && !is_subclass_of($data, JsonSerializable::class)) {
+            throw new InvalidArgumentException(
+                'Data must be an array or implement JsonSerializable interface'
+            );
+        }
+        $response = response_factory()->createResponse($status);
+        $response->getBody()->write(json_encode($data, $flags));
+        if (json_last_error() !== JSON_ERROR_NONE) {
+            throw new InvalidArgumentException(
+                sprintf('Unable to encode data to JSON: %s', json_last_error_msg())
+            );
+        }
+        return $response->withHeader('Content-Type', 'application/json');
+    }
+}
+
+if (!function_exists('redirect')) {
+
+    /**
+     * Creates a redirect response.
+     *
+     * @param string $url The URL to redirect to.
+     * @param int $status The HTTP status code.
+     * @return ResponseInterface The redirect response.
+     */
+    function redirect(string $url, int $status = 302): ResponseInterface
+    {
+        $response = response_factory()->createResponse($status);
+        return $response->withHeader('Location', $url);
+    }
+}
+
+if (!function_exists('render_view')) {
+
+    /**
+     * Renders a view template with the provided context.
+     *
+     * @param string $view The name of the view template.
+     * @param array $context The context data to pass to the view.
+     * @return string The rendered view.
+     * @throws ContainerExceptionInterface
+     * @throws NotFoundExceptionInterface
+     */
+    function render_view(string $view, array $context = []): string
+    {
+        if (!container()->has('render')) {
+            throw new LogicException('The "render_view" method requires a Renderer to be available. You can choose between installing "Michel/php-renderer" or "twig/twig" depending on your preference.');
+        }
+
+        $renderer = container()->get('render');
+        return $renderer->render($view, $context);
+    }
+}
+
+if (!function_exists('render')) {
+
+    /**
+     * Renders a view template and creates an HTTP response.
+     *
+     * @param string $view The name of the view template.
+     * @param array $context The context data to pass to the view.
+     * @param int $status The HTTP status code.
+     * @return ResponseInterface The HTTP response with the rendered view.
+     * @throws ContainerExceptionInterface
+     * @throws NotFoundExceptionInterface
+     */
+    function render(string $view, array $context = [], int $status = 200): ResponseInterface
+    {
+        return response(render_view($view, $context), $status);
+    }
+}

+ 70 - 0
functions/helpers_array.php

@@ -0,0 +1,70 @@
+<?php
+
+if (!function_exists('array_flatten')) {
+
+    /**
+     * @param array $array
+     * @return array
+     */
+    function array_flatten(array $array): array
+    {
+        $result = [];
+        foreach ($array as $key => $value) {
+            if (is_array($value)) {
+                $result = array_merge($result, array_flatten($value));
+                continue;
+            }
+            $result[$key] = $value;
+        }
+        return $result;
+    }
+}
+
+if (!function_exists('array_dot')) {
+
+    /**
+     * Flatten a multi-dimensional associative array with dots.
+     *
+     * @param array $array The array to flatten.
+     * @param string $rootKey The base key prefix (used internally for recursion).
+     * @return array The flattened array with dot notation keys.
+     */
+    function array_dot(array $array, string $rootKey = ''): array
+    {
+        $result = [];
+        foreach ($array as $key => $value) {
+            $key = strval($key);
+            $key = $rootKey !== '' ? ($rootKey . '.' . $key) : $key;
+            if (is_array($value)) {
+                $result = $result + array_dot($value, $key);
+                continue;
+            }
+            $result[$key] = $value;
+        }
+
+        return $result;
+    }
+}
+
+if (!function_exists('array_group_by')) {
+
+    /**
+     * @param array $array
+     * @param string $key
+     * @return array
+     */
+    function array_group_by(array $array, string $key): array
+    {
+        $result = [];
+        foreach ($array as $value) {
+            $group = $value;
+            if (is_array( $value)) {
+                $group = $value[$key];
+            }elseif (is_object($value)) {
+                $group = $value->$key;
+            }
+            $result[$group][] = $value;
+        }
+        return $result;
+    }
+}

+ 49 - 0
functions/helpers_date.php

@@ -0,0 +1,49 @@
+<?php
+
+if (!function_exists('days_between')) {
+
+    function days_between(DateTime $datetime1, DateTime $datetime2): int
+    {
+        $interval = $datetime1->diff($datetime2);
+        return $interval->days;
+    }
+}
+
+if (!function_exists('is_leap_year')) {
+    function is_leap_year(DateTime $date): bool
+    {
+        $year = $date->format('Y');
+        return ($year % 4 === 0 && $year % 100 !== 0) || ($year % 400 === 0);
+    }
+}
+
+if (!function_exists('is_weekend')) {
+
+    /**
+     * @param DateTime $date
+     * @return bool
+     */
+    function is_weekend(DateTime $date): bool
+    {
+        return in_array($date->format('N'), [6, 7]);
+    }
+}
+
+if (!function_exists('is_today')) {
+
+    /**
+     * @param DateTime $date
+     * @return bool
+     */
+    function is_today(DateTime $date): bool
+    {
+        return $date->format('Y-m-d') === (new DateTime())->format('Y-m-d');
+    }
+}
+
+if (!function_exists('is_past')) {
+    function is_past(DateTime $date): bool
+    {
+        return (new DateTime()) > $date;
+    }
+}

+ 28 - 0
functions/helpers_file.php

@@ -0,0 +1,28 @@
+<?php
+
+if (!function_exists('filepath_join')) {
+
+    /**
+     * @param ...$paths
+     * @return string
+     */
+    function filepath_join(...$paths): string
+    {
+        $cleanedPaths = [];
+        foreach ($paths as $path) {
+            $path = trim($path);
+            $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
+            if (empty($path)) {
+                continue;
+            }
+
+            $path = rtrim($path, DIRECTORY_SEPARATOR);
+            $cleanedPaths[] = $path;
+        }
+
+        return implode(DIRECTORY_SEPARATOR, $cleanedPaths);
+    }
+}
+
+
+

+ 64 - 0
functions/helpers_string.php

@@ -0,0 +1,64 @@
+<?php
+
+if (!function_exists('__e')) {
+
+    /**
+     * Encodes a string for HTML entities.
+     *
+     * @param string $str The string to encode.
+     * @param int $flags Flags for htmlentities.
+     * @param string $encoding The character encoding.
+     * @return string The encoded string.
+     */
+    function __e(string $str, int $flags = ENT_QUOTES, string $encoding = 'UTF-8'): string
+    {
+        return htmlentities($str, $flags, $encoding);
+    }
+}
+
+if (!function_exists('str_starts_with')) {
+
+    /**
+     * @param string $haystack
+     * @param string $needle
+     * @return bool
+     */
+    function str_starts_with(string $haystack, string $needle): bool
+    {
+        return substr($haystack, 0, strlen($needle)) === $needle;
+    }
+}
+
+if (!function_exists('str_ends_with')) {
+
+    /**
+     * @param string $haystack
+     * @param string $needle
+     * @return bool
+     */
+    function str_ends_with(string $haystack, string $needle): bool
+    {
+        return substr($haystack, -strlen($needle)) === $needle;
+    }
+}
+
+if (!function_exists('str_contains')) {
+
+    /**
+     * @param string $haystack
+     * @param string $needle
+     * @return bool
+     */
+    function str_contains(string $haystack, string $needle): bool
+    {
+        return strpos($haystack, $needle) !== false;
+    }
+}
+
+if (!function_exists('_m_convert')) {
+    function _m_convert($size): string
+    {
+        $unit = array('B', 'KB', 'MB', 'GB', 'TB', 'PB');
+        return @round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . $unit[$i];
+    }
+}

+ 243 - 0
resources/debug/debugbar.html.php

@@ -0,0 +1,243 @@
+<?php
+
+/**
+ * @var array $profiler [
+ * '@timestamp' => (new DateTimeImmutable())->format('c'),
+ * 'log.level' => 'debug',
+ * 'id' => $request->getAttribute('request_id', 'unknown'),
+ * 'event.duration' => $duration,
+ * 'metrics' => [
+ * 'memory.usage' => $this->convertMemory(memory_get_usage(true)),
+ * 'memory.peak' => $this->convertMemory(memory_get_peak_usage(true)),
+ * 'load_time.ms' => $duration * 1000,
+ * 'load_time.s' => number_format($duration, 3),
+ * ],
+ * 'http.request' => [
+ * 'method' => $request->getMethod(),
+ * 'url' => $request->getUri()->__toString(),
+ * 'path' => $request->getUri()->getPath(),
+ * 'body' => $request->getBody()->getContents(),
+ * 'headers' => $request->getHeaders(),
+ * 'query' => $request->getQueryParams(),
+ * 'post' => $request->getParsedBody() ?? [],
+ * 'cookies' => $request->getCookieParams(),
+ * 'protocol' => $request->getProtocolVersion(),
+ * 'server' => $request->getServerParams(),
+ * ],
+ * ]
+ */
+?>
+<style>
+    .__michel_debug_navbar {
+        background-color: #1e232d;
+        position: fixed;
+        right: 0;
+        bottom: 0;
+        width: 100%;
+        display: flex;
+        height: 40px;
+        font-family: Inter, sans-serif;
+        white-space: nowrap;
+    }
+
+    .__michel_debug_navbar a {
+        color: rgb(166, 223, 239);
+        display: block;
+        text-align: center;
+        padding: 12px 14px;
+        text-decoration: none;
+        font-size: 12px;
+    }
+
+    .__michel_debug_navbar a:hover {
+        background-color: #07193e;
+    }
+
+    .__michel_debug_navbar a:hover > .__michel_debug_value {
+        transform: scale(1.05);
+    }
+
+    .__michel_debug_value {
+        font-weight: bold;
+        font-size: 11px;
+        color: #ececec;
+        display: inline-block;
+    }
+
+    .__michel_dropup {
+        position: relative;
+        display: inline-block;
+    }
+
+    .__michel_dropup-content {
+        font-size: 12px;
+        display: none;
+        position: absolute;
+        background-color: #1e232d;
+        max-width: 450px;
+        max-height: 480px;
+        bottom: 40px;
+        overflow: hidden;
+        overflow-y: auto;
+        padding: 10px;
+        z-index: 100000;
+        vertical-align: baseline;
+        letter-spacing: normal;
+        white-space: nowrap;
+
+    }
+
+    .__michel_dropup-content a:hover {
+        background-color: inherit;
+    }
+
+    .__michel_dropup:hover .__michel_dropup-content {
+        display: block;
+    }
+
+    .__michel_table {
+        border-collapse: collapse;
+        border-spacing: 0;
+        width: 100%;
+        border: 0px;
+    }
+
+    .__michel_table th, td {
+        text-align: left;
+        padding: 6px;
+    }
+
+    .__michel_label {
+        padding: 4px;
+    }
+
+    .__michel_label_success {
+        background-color: #04AA6D;
+    }
+
+    /* Green */
+    .__michel_label_info {
+        background-color: #2196F3;
+    }
+
+    /* Blue */
+    .__michel_label_warning {
+        background-color: #ff9800;
+    }
+
+    /* Orange */
+    .__michel_label_danger {
+        background-color: #f44336;
+    }
+
+    /* Red */
+    .__michel_label_other {
+        background-color: #e7e7e7;
+        color: black;
+    }
+
+    /* Gray */
+</style>
+<div class="__michel_debug_navbar">
+    <div class="__michel_dropup">
+        <a href="#response">
+            <?php if ($profiler['__response_code'] >= 200 && $profiler['__response_code'] < 300) : ?>
+                🚦 <span class="__michel_label __michel_label_success"><span
+                            class="__michel_debug_value"><?php echo $profiler['__response_code'] ?></span></span>
+            <?php elseif ($profiler['__response_code'] >= 300 && $profiler['__response_code'] < 400) : ?>
+                🚦 <span class="__michel_label __michel_label_info"><span
+                            class="__michel_debug_value"><?php echo $profiler['__response_code'] ?></span></span>
+            <?php elseif ($profiler['__response_code'] >= 400 && $profiler['__response_code'] < 500) : ?>
+                🚦 <span class="__michel_label __michel_label_warning"><span
+                            class="__michel_debug_value"><?php echo $profiler['__response_code'] ?></span></span>
+            <?php else : ?>
+                🚦 <span class="__michel_label __michel_label_danger"><span
+                            class="__michel_debug_value"><?php echo $profiler['__response_code'] ?></span></span>
+            <?php endif; ?>
+        </a>
+        <div class="__michel_dropup-content">
+            <table class="__michel_table">
+                <tr>
+                    <td>HTTP status <Ver></Ver></td>
+                    <td class="__michel_debug_value" title="<?php echo $profiler['__response_code'] ?>"><?php echo $profiler['__response_code'] ?></td>
+                </tr>
+                <?php if (isset($profiler['__controller'])) : ?>
+                    <tr>
+                        <td>Controller</td>
+                        <td class="__michel_debug_value" title="<?php echo $profiler['__controller'] ?>"><?php echo $profiler['__controller'] ?></td>
+                    </tr>
+                <?php endif; ?>
+                <?php if (isset($profiler['__route_name'])) : ?>
+                    <tr>
+                        <td>Route name</td>
+                        <td class="__michel_debug_value" title="<?php echo $profiler['__route_name'] ?>"><?php echo $profiler['__route_name'] ?></td>
+                    </tr>
+                <?php endif; ?>
+            </table>
+        </div>
+    </div>
+    <a href="#time">
+        🕒 [REQ] <span class="__michel_debug_value"><?php echo $profiler['metrics']['load_time.ms'] ?> ms</span>
+    </a>
+    <a href="#memory">
+        💾 [MEM] <span class="__michel_debug_value"><?php echo $profiler['metrics']['memory.peak.human'] ?></span>
+    </a>
+    <a href="#request">
+        🌐 [METHOD] <span class="__michel_debug_value"><?php echo $profiler['http.request']['method'] ?></span>
+    </a>
+    <a href="#env">
+        🛠️ [ENV] <span class="__michel_debug_value"><?php echo strtoupper($profiler['environment']) ?></span>
+    </a>
+    <div class="__michel_dropup">
+        <a href="#">
+            🐘 [PHP] <span class="__michel_debug_value"><?php echo $profiler['php_version'] ?></span>
+        </a>
+        <div class="__michel_dropup-content">
+            <table class="__michel_table">
+                <tr>
+                    <td>PHP Version</td>
+                    <td class="__michel_debug_value"
+                        title="<?php echo $profiler['php_version'] ?>"><?php echo $profiler['php_version'] ?></td>
+                </tr>
+                <tr>
+                    <td>PHP Extensions</td>
+                    <td class="__michel_debug_value"
+                        title="<?php echo $profiler['php_extensions'] ?>"><?php echo $profiler['php_extensions'] ?></td>
+                </tr>
+                <tr>
+                    <td>PHP SAPI</td>
+                    <td class="__michel_debug_value"
+                        title="<?php echo $profiler['php_sapi'] ?>"><?php echo $profiler['php_sapi'] ?></td>
+                </tr>
+                <tr>
+                    <td>PHP Memory Limit</td>
+                    <td class="__michel_debug_value"
+                        title="<?php echo $profiler['php_memory_limit'] ?>"><?php echo $profiler['php_memory_limit'] ?></td>
+                </tr>
+                <tr>
+                    <td>PHP Timezone</td>
+                    <td class="__michel_debug_value"
+                        title="<?php echo $profiler['php_timezone'] ?>"><?php echo $profiler['php_timezone'] ?></td>
+                </tr>
+            </table>
+        </div>
+    </div>
+    <?php if (isset($profiler['__middlewares_executed'])) : ?>
+        <div class="__michel_dropup">
+            <a href="#" title="Middlewares executed">
+                🔀 [MID] <span class="__michel_debug_value"><?php echo count($profiler['__middlewares_executed']) ?></span>
+            </a>
+            <div class="__michel_dropup-content">
+                <table class="__michel_table">
+                    <?php foreach ($profiler['__middlewares_executed'] as $index => $middleware) : ?>
+                        <tr>
+                            <td>
+                                <span class="__michel_debug_value"><?php echo sprintf('%s. %s', $index + 1, $middleware) ?></span>
+                            </td>
+                        </tr>
+                    <?php endforeach; ?>
+                </table>
+            </div>
+        </div>
+    <?php endif; ?>
+</div>

File diff suppressed because it is too large
+ 28 - 0
resources/views/error.html.php


+ 111 - 0
src/App.php

@@ -0,0 +1,111 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Michel\Framework\Core;
+
+use Michel\Resolver\Option;
+use Michel\Resolver\OptionsResolver;
+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)
+    {
+        $resolver = new OptionsResolver([
+            Option::mixed('server_request')->validator(static function ($value) {
+                return $value instanceof \Closure;
+            }),
+            Option::mixed('server_request_factory')->validator(static function ($value) {
+                return $value instanceof \Closure;
+            }),
+            Option::mixed('response_factory')->validator(static function ($value) {
+                return $value instanceof \Closure;
+            }),
+            Option::mixed('container')->validator(static function ($value) {
+                return $value instanceof \Closure;
+            }),
+            Option::array('custom_environments')->validator(static function (array $value) {
+                $environmentsFiltered = array_filter($value, function ($value) {
+                    return is_string($value) === false;
+                });
+                if ($environmentsFiltered !== []) {
+                    throw new \InvalidArgumentException('custom_environments array values must be string only');
+                }
+                return true;
+            })->setOptional([]),
+        ]);
+        $this->options = $resolver->resolve($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;
+    }
+}

+ 285 - 0
src/BaseKernel.php

@@ -0,0 +1,285 @@
+<?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\HttpExceptionInterface;
+use InvalidArgumentException;
+use Michel\Framework\Core\Routing\ControllerFinder;
+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 = '1.0.0-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);
+
+            $requestHandler = new RequestHandler($this->container, $this->middlewareCollection);
+            return $requestHandler->handle($request);
+        } 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'),
+            'http.request' => [
+                'method' => $request->getMethod(),
+                'url' => $request->getUri()->__toString(),
+            ],
+            'message' => $exception->getMessage(),
+            'error' => [
+                'code' => $exception->getCode(),
+                'stack_trace' => $exception->getTrace(),
+                'class' => get_class($exception),
+            ],
+            'source' => [
+                'file' => $exception->getFile(),
+                'line' => $exception->getLine(),
+            ],
+        ]);
+    }
+
+    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);
+        }
+        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
+    {
+        if ($this->getEnv() === 'dev') {
+            ErrorHandler::register();
+            return;
+        }
+        ini_set("log_errors", '1');
+        ini_set("error_log", $this->getLogDir() . '/error_log.log');
+
+        ini_set("display_startup_errors", '0');
+        ini_set("display_errors", '0');
+        ini_set("html_errors", '0');
+        ini_set("track_errors", '0');
+
+        error_reporting(E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR);
+    }
+
+    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) = (new Dependency($this))->load();
+        $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 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;
+    }
+}

+ 43 - 0
src/Command/AbstractMakeCommand.php

@@ -0,0 +1,43 @@
+<?php
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Command;
+
+use RuntimeException;
+
+abstract class AbstractMakeCommand
+{
+    abstract protected function template(string $classNamespace, string $curtClassName): string;
+
+    protected function createClass(string $className): string
+    {
+        $namespaceArray = explode('\\', $className);
+        $curtClassName = array_pop($namespaceArray);
+
+        $classFilePath = self::getFilePathFromPsr4($className);
+        $classNamespace = rtrim(substr($className, 0, -strlen($curtClassName)), '\\');
+
+        if (!is_dir(dirname($classFilePath))) {
+            mkdir(dirname($classFilePath), 0777, true);
+        }
+
+        file_put_contents($classFilePath, $this->template($classNamespace, $curtClassName));
+
+        return $classFilePath;
+    }
+
+    private static function getFilePathFromPsr4(string $controllerName): string
+    {
+        $loader = michel_composer_loader();
+        foreach ($loader->getPrefixesPsr4() as $namespace => $paths) {
+            foreach ($paths as $path) {
+                if (strpos($controllerName, $namespace) === 0) {
+                    $path = realpath($path);
+                    return filepath_join( $path, str_replace('\\', DIRECTORY_SEPARATOR, substr($controllerName, strlen($namespace))) . '.php');
+                }
+            }
+        }
+
+        throw new RuntimeException('Unable to determine the namespace.');
+    }
+}

+ 76 - 0
src/Command/CacheClearCommand.php

@@ -0,0 +1,76 @@
+<?php
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Command;
+
+use Michel\Console\Command\CommandInterface;
+use Michel\Console\InputInterface;
+use Michel\Console\Output\ConsoleOutput;
+use Michel\Console\OutputInterface;
+use Psr\Container\ContainerInterface;
+
+final class CacheClearCommand implements CommandInterface
+{
+    private ContainerInterface $container;
+
+    public function __construct(ContainerInterface $container)
+    {
+        $this->container = $container;
+    }
+
+    public function getName(): string
+    {
+        return 'cache:clear';
+    }
+
+    public function getDescription(): string
+    {
+        return 'Clear the cache';
+    }
+
+    public function getOptions(): array
+    {
+        return [];
+    }
+
+    public function getArguments(): array
+    {
+        return [];
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): void
+    {
+        $io = new ConsoleOutput($output);
+
+        $realCacheDir = $this->container->get('michel.cache_dir');
+        if (!is_writable($realCacheDir)) {
+            throw new \RuntimeException(sprintf('Unable to write in the "%s" directory.', $realCacheDir));
+        }
+
+        $io->title('Clearing the cache : ' . $realCacheDir);
+
+        $files = new \RecursiveIteratorIterator(
+            new \RecursiveDirectoryIterator($realCacheDir, \FilesystemIterator::SKIP_DOTS),
+            \RecursiveIteratorIterator::CHILD_FIRST
+        );
+        /**
+         * @var \SplFileInfo $file
+         */
+        foreach ($files as $file) {
+            if ($file->getFilename() === '.gitignore') {
+                continue;
+            }
+            if ($file->isFile()) {
+                if (!unlink($file->getPathname())) {
+                    throw new \RuntimeException("Failed to unlink {$file->getPathname()} : " . var_export(error_get_last(), true));
+                }
+            }elseif ($file->isDir()) {
+                if (!rmdir($file->getPathname())) {
+                    throw new \RuntimeException("Failed to remove {$file->getPathname()} : " . var_export(error_get_last(), true));
+                }
+            }
+        }
+
+        $io->success('Cache was successfully cleared.');
+    }
+}

+ 86 - 0
src/Command/DebugContainerCommand.php

@@ -0,0 +1,86 @@
+<?php
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Command;
+
+use Michel\Console\Command\CommandInterface;
+use Michel\Console\InputInterface;
+use Michel\Console\Output\ConsoleOutput;
+use Michel\Console\OutputInterface;
+use Psr\Container\ContainerInterface;
+
+final class DebugContainerCommand implements CommandInterface
+{
+    private ContainerInterface $container;
+
+    public function __construct(ContainerInterface $container)
+    {
+        $this->container = $container;
+    }
+
+    public function getName(): string
+    {
+        return 'debug:container';
+    }
+
+    public function getDescription(): string
+    {
+        return 'List all service IDs registered in the container';
+    }
+
+    public function getOptions(): array
+    {
+        return [];
+    }
+
+    public function getArguments(): array
+    {
+        return [];
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): void
+    {
+        $io = new ConsoleOutput($output);
+
+        $io->title('Registered Service IDs');
+
+        $serviceIds = $this->container->get('michel.services_ids');
+        natcasesort($serviceIds);
+
+        $io->table(
+            ['Service ID', 'Value'],
+            array_map(function ($serviceId) {
+                $value = null;
+                if ($this->container->has($serviceId)) {
+                    try {
+                        $value = $this->variableToString($this->container->get($serviceId));
+                    } catch (\Throwable $e) {
+                        $value = $this->variableToString($e->getMessage());
+                    }
+
+                }
+                return [$serviceId, $value];
+            }, $serviceIds)
+        );
+
+        $io->writeln('');
+
+    }
+
+    private function variableToString($variable): string
+    {
+        if (is_object($variable)) {
+            return 'Object: ' . get_class($variable);
+        } elseif (is_array($variable)) {
+            $variables = [];
+            foreach ($variable as $item) {
+                $variables[] = $this->variableToString($item);
+            }
+            return print_r($variables, true);
+        } elseif (is_resource($variable)) {
+            return (string)$variable;
+        }
+
+        return var_export($variable, true);
+    }
+}

+ 53 - 0
src/Command/DebugEnvCommand.php

@@ -0,0 +1,53 @@
+<?php
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Command;
+
+use Michel\Console\Command\CommandInterface;
+use Michel\Console\InputInterface;
+use Michel\Console\Output\ConsoleOutput;
+use Michel\Console\OutputInterface;
+
+final class DebugEnvCommand implements CommandInterface
+{
+    public function getName(): string
+    {
+        return 'debug:env';
+    }
+
+    public function getDescription(): string
+    {
+      return 'Lists all environment variables along with their corresponding values.';
+    }
+
+    public function getOptions(): array
+    {
+        return [];
+    }
+
+    public function getArguments(): array
+    {
+        return [];
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): void
+    {
+        $io = new ConsoleOutput($output);
+
+        $io->title('Env Variables');
+
+        $values = [];
+        foreach ($_ENV as $key => $value) {
+            $values[] = [$key, $value];
+        }
+        $io->table(
+            ['Variable', 'Value'],
+            $values
+        );
+
+        $io->writeln('');
+        $io->writeln('Please note that actual values may vary between web and command-line interfaces.');
+        $io->writeln('');
+    }
+
+}

+ 70 - 0
src/Command/DebugRouteCommand.php

@@ -0,0 +1,70 @@
+<?php
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Command;
+
+use Michel\Console\Command\CommandInterface;
+use Michel\Console\InputInterface;
+use Michel\Console\Output\ConsoleOutput;
+use Michel\Console\OutputInterface;
+use Michel\Route;
+use Psr\Container\ContainerInterface;
+
+final class DebugRouteCommand implements CommandInterface
+{
+    private ContainerInterface $container;
+
+    public function __construct(ContainerInterface $container)
+    {
+        $this->container = $container;
+    }
+
+    public function getName(): string
+    {
+        return 'debug:routes';
+    }
+
+    public function getDescription(): string
+    {
+        return 'List all registered routes in the application';
+    }
+
+    public function getOptions(): array
+    {
+        return [];
+    }
+
+    public function getArguments(): array
+    {
+        return [];
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): void
+    {
+        $io = new ConsoleOutput($output);
+
+        $io->title('Registered Routes');
+
+        /**
+         * @var array<Route> $routes
+         */
+        $routes = $this->container->get('michel.routes');
+        $formattedRoutes = array_map(function (Route $route) {
+            return [
+                'Name' => $route->getName(),
+                'Method' => implode('|', $route->getMethods()),
+                'Path' => $route->getPath(),
+            ];
+        }, $routes);
+
+        uasort($formattedRoutes, function ($a, $b) {
+            return strcasecmp($a['Name'], $b['Name']);
+        });
+
+        $io->table(
+            ['Name', 'Method', 'Path'],
+            $formattedRoutes
+        );
+        $io->writeln('');
+    }
+}

+ 76 - 0
src/Command/LogClearCommand.php

@@ -0,0 +1,76 @@
+<?php
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Command;
+
+use Michel\Console\Command\CommandInterface;
+use Michel\Console\InputInterface;
+use Michel\Console\Output\ConsoleOutput;
+use Michel\Console\OutputInterface;
+use Psr\Container\ContainerInterface;
+
+final class LogClearCommand implements CommandInterface
+{
+    private ContainerInterface $container;
+
+    public function __construct(ContainerInterface $container)
+    {
+        $this->container = $container;
+    }
+
+    public function getName(): string
+    {
+        return 'log:clear';
+    }
+
+    public function getDescription(): string
+    {
+        return 'Deletes all log files in the specified log directory.';
+    }
+
+    public function getOptions(): array
+    {
+        return [];
+    }
+
+    public function getArguments(): array
+    {
+        return [];
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): void
+    {
+        $io = new ConsoleOutput($output);
+
+        $logDir = $this->container->get('michel.logs_dir');
+        if (!is_writable($logDir)) {
+            throw new \RuntimeException(sprintf('Unable to write in the "%s" directory.', $logDir));
+        }
+
+        $io->title(sprintf('Clearing log files in directory: %s', $logDir));
+
+        $files = new \RecursiveIteratorIterator(
+            new \RecursiveDirectoryIterator($logDir, \FilesystemIterator::SKIP_DOTS),
+            \RecursiveIteratorIterator::CHILD_FIRST
+        );
+        /**
+         * @var \SplFileInfo $file
+         */
+        foreach ($files as $file) {
+            if ($file->getFilename() === '.gitignore') {
+                continue;
+            }
+            if ($file->isFile()) {
+                if (!unlink($file->getPathname())) {
+                    throw new \RuntimeException("Failed to unlink {$file->getPathname()} : " . var_export(error_get_last(), true));
+                }
+            }elseif ($file->isDir()) {
+                if (!rmdir($file->getPathname())) {
+                    throw new \RuntimeException("Failed to remove {$file->getPathname()} : " . var_export(error_get_last(), true));
+                }
+            }
+        }
+
+        $io->success('All log files have been successfully cleared.');
+    }
+}

+ 91 - 0
src/Command/MakeCommandCommand.php

@@ -0,0 +1,91 @@
+<?php
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Command;
+
+use Michel\Console\Argument\CommandArgument;
+use Michel\Console\Command\CommandInterface;
+use Michel\Console\InputInterface;
+use Michel\Console\Output\ConsoleOutput;
+use Michel\Console\OutputInterface;
+
+final class MakeCommandCommand extends AbstractMakeCommand implements CommandInterface
+{
+
+    public function getName(): string
+    {
+        return 'make:command';
+    }
+
+    public function getDescription(): string
+    {
+        return 'Generate a new command';
+    }
+
+    public function getOptions(): array
+    {
+        return [];
+    }
+
+    public function getArguments(): array
+    {
+       return [
+           new CommandArgument("name", true, null, "The name of the command, ex : App\\Command\\CreateUserCommand")
+       ];
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): void
+    {
+        $io = new ConsoleOutput($output);
+        $commandName = $input->getArgumentValue('name');
+
+        $filename = $this->createClass($commandName);
+        $io->success("Class $commandName created successfully at $filename.");
+    }
+
+    protected function template(string $classNamespace, string $curtClassName): string
+    {
+        return <<<PHP
+<?php
+
+namespace $classNamespace;
+
+use Michel\Console\Command\CommandInterface;
+use Michel\Console\InputInterface;
+use Michel\Console\Output\ConsoleOutput;
+use Michel\Console\OutputInterface;
+
+final class $curtClassName implements CommandInterface
+{
+
+    public function getName(): string
+    {
+        return 'my:command';
+    }
+
+    public function getDescription(): string
+    {
+        return 'Description of my command';
+    }
+
+    public function getOptions(): array
+    {
+        return [];
+    }
+
+    public function getArguments(): array
+    {
+       return [];
+    }
+    
+    public function execute(InputInterface \$input, OutputInterface \$output): void
+    {
+        \$io = new ConsoleOutput(\$output);
+        \$io->success("successfully message");
+    }
+}
+PHP;
+
+    }
+
+}

+ 68 - 0
src/Command/MakeControllerCommand.php

@@ -0,0 +1,68 @@
+<?php
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Command;
+
+use Michel\Console\Argument\CommandArgument;
+use Michel\Console\Command\CommandInterface;
+use Michel\Console\InputInterface;
+use Michel\Console\Output\ConsoleOutput;
+use Michel\Console\OutputInterface;
+
+final class MakeControllerCommand extends AbstractMakeCommand implements CommandInterface
+{
+
+    public function getName(): string
+    {
+        return 'make:controller';
+    }
+
+    public function getDescription(): string
+    {
+        return 'Generate a new controller';
+    }
+
+    public function getOptions(): array
+    {
+        return [];
+    }
+
+    public function getArguments(): array
+    {
+        return [
+            new CommandArgument("name", true, null, "The name of the controller, ex : App\\Controller\\MainController")
+        ];
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): void
+    {
+        $io = new ConsoleOutput($output);
+        $controllerName = $input->getArgumentValue('name');
+
+        $filename = $this->createClass($controllerName);
+        $io->success("Class $controllerName created successfully at $filename.");
+
+    }
+
+    protected function template(string $classNamespace, $curtClassName): string
+    {
+        return <<<PHP
+<?php
+
+namespace $classNamespace;
+
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use Michel\Framework\Core\Controller\Controller;
+
+final class $curtClassName extends Controller
+{
+    public function __invoke(ServerRequestInterface \$request): ResponseInterface
+    {
+        // TODO: Implement controller logic here
+    }
+}
+PHP;
+    }
+
+}

+ 73 - 0
src/Config/ConfigProvider.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace Michel\Framework\Core\Config;
+
+use Psr\Container\ContainerInterface;
+
+final class ConfigProvider
+{
+    private ContainerInterface $container;
+
+    public function __construct(ContainerInterface $container)
+    {
+        $this->container = $container;
+    }
+
+    public function getTemplateDir(): string
+    {
+        $templateDir = $this->container->get('app.template_dir');
+        if (!is_string($templateDir)) {
+            throw new \LogicException(sprintf('The "app.template_dir" configuration must be a string. Given: %s', gettype($templateDir)));
+        }
+
+        if (!str_starts_with($templateDir, '/')) {
+            $templateDir = filepath_join($this->container->get('michel.project_dir'), $templateDir);
+        }
+
+        if (!is_dir($templateDir)) {
+            throw new \LogicException(sprintf('The specified "app.template_dir" directory does not exist: "%s".', $templateDir));
+        }
+
+        return $templateDir;
+    }
+
+    public function getAllowedIps(): array
+    {
+        $allowedIps = $this->container->get('app.allowed_ips');
+
+        if (is_string($allowedIps)) {
+            $allowedIps = explode(',', $allowedIps);
+        }
+
+        if (!is_array($allowedIps)) {
+            throw new \LogicException('The "app.allowed_ips" should be an array of IP addresses');
+        }
+
+        $allowedIps = array_filter($allowedIps);
+
+        foreach ($allowedIps as $value) {
+            if (!filter_var($value, FILTER_VALIDATE_IP)) {
+                throw new \LogicException(sprintf('Invalid IP address detected: "%s". Ensure all values in allowed IPs are valid IP addresses.', $value));
+            }
+        }
+        return $allowedIps;
+    }
+
+    public function isForceHttps(): bool
+    {
+        $forceHttps = $this->container->get('app.force_https');
+        if (!is_bool($forceHttps)) {
+            throw new \LogicException('The "app.force_https" should be a boolean value');
+        }
+        return $forceHttps;
+    }
+
+    public function isMaintenance(): bool
+    {
+        $maintenance = $this->container->get('app.maintenance');
+        if (!is_bool($maintenance)) {
+            throw new \LogicException('The "app.maintenance" should be a boolean value');
+        }
+        return $maintenance;
+    }
+}

+ 48 - 0
src/Controller/Controller.php

@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Controller;
+
+use InvalidArgumentException;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Server\MiddlewareInterface;
+
+/**
+ * @author Michel.F 
+ */
+abstract class Controller
+{
+    private ContainerInterface $container;
+    protected array $middlewares = [];
+
+    public function setContainer(ContainerInterface $container): void
+    {
+        $this->container = $container;
+    }
+
+    /**
+     * @return array<MiddlewareInterface, string>
+     */
+    public function getMiddlewares(): array
+    {
+        return $this->middlewares;
+    }
+
+    /***
+     * @param $middleware MiddlewareInterface|string
+     * @return void
+     */
+    protected function middleware($middleware): void
+    {
+        if (!$middleware instanceof MiddlewareInterface && (!is_string($middleware) || !class_exists($middleware))) {
+            throw new InvalidArgumentException('The Middleware must be Class name or an instance of Psr\Http\Message\ResponseInterface.');
+        }
+        $this->middlewares[] = $middleware;
+    }
+
+    protected function get(string $id)
+    {
+        return $this->container->get($id);
+    }
+}

+ 35 - 0
src/Debug/DebugDataCollector.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace Michel\Framework\Core\Debug;
+
+final class DebugDataCollector
+{
+    private array $data = [];
+    private bool $isEnabled;
+
+    public function __construct(bool $isEnabled = false)
+    {
+        $this->isEnabled = $isEnabled;
+    }
+
+    public function add(string $key, $value): void
+    {
+        if (!$this->isEnabled) {
+            return;
+        }
+        $this->data[$key] = $value;
+    }
+
+    public function push(string $key, $value): void
+    {
+        if (!$this->isEnabled) {
+            return;
+        }
+        $this->data[$key][] = $value;
+    }
+
+    public function getData(): array
+    {
+        return $this->data;
+    }
+}

+ 60 - 0
src/Debug/ExecutionProfiler.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace Michel\Framework\Core\Debug;
+
+use DateTimeImmutable;
+use LogicException;
+
+final class ExecutionProfiler
+{
+    private bool $isStarted = false;
+    private float $startTime;
+    private int $startMemory;
+    private array $metadata;
+
+    public function __construct(array $metadata = [])
+    {
+        $this->metadata = $metadata;
+    }
+
+    public function start(): void
+    {
+        if ($this->isStarted) {
+            throw new LogicException('Please call stop() before start()');
+        }
+        $this->isStarted = true;
+        $this->startTime = microtime(true);
+        $this->startMemory = memory_get_usage(true);
+    }
+
+    public function addMetadata(string $key, $value)
+    {
+        $this->metadata[$key][] = $value;
+    }
+
+    public function stop(): array
+    {
+        if ($this->isStarted === false) {
+            throw new LogicException('Please call start() before stop()');
+        }
+
+        $endTime = microtime(true);
+        $endMemory = memory_get_usage(true);
+
+        $duration = $endTime - $this->startTime;
+        $memoryUsage = $endMemory - $this->startMemory;
+
+        $this->isStarted = false;
+        return [
+                '@timestamp' => (new DateTimeImmutable())->format('c'),
+                'log.level' => 'debug',
+                'event.duration' => $duration,
+                'metrics' => [
+                    'memory.usage' => _m_convert($memoryUsage),
+                    'peak_memory.usage' => _m_convert(memory_get_peak_usage(true)),
+                    'load_time.ms' => $duration * 1000,
+                    'load_time.s' => number_format($duration, 3),
+                ],
+            ] + $this->metadata;
+    }
+}

+ 88 - 0
src/Debug/RequestProfiler.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace Michel\Framework\Core\Debug;
+
+use DateTimeImmutable;
+use LogicException;
+use Psr\Http\Message\ServerRequestInterface;
+
+final class RequestProfiler
+{
+    private bool $isStarted = false;
+    private float $startTime;
+    private ServerRequestInterface $request;
+
+    private array $metadata;
+
+    public function __construct(array $metadata = [])
+    {
+        $this->metadata = $metadata;
+    }
+
+    public function start(ServerRequestInterface $request): void
+    {
+        if ($this->isStarted) {
+            throw new LogicException('Please call stop() before start()');
+        }
+
+        $this->isStarted = true;
+        $this->startTime = microtime(true);
+        $this->request = $request;
+    }
+
+    public function withDebugDataCollector(DebugDataCollector $debugDataCollector): self
+    {
+        foreach ($debugDataCollector->getData() as $key => $value) {
+            $this->metadata["__$key"] = $value;
+        }
+        return $this;
+    }
+
+    public function stop(): array
+    {
+        if ($this->isStarted === false) {
+            throw new LogicException('Please call start() before stop()');
+        }
+
+        $endTime = microtime(true);
+        $duration = $endTime - $this->startTime;
+
+        $log = $this->createLog($duration);
+
+        $this->isStarted = false;
+        return array_merge($log, $this->metadata);
+    }
+
+    private function createLog(float $duration): array
+    {
+        $request = $this->request;
+        $memoryUsage = memory_get_usage(true);
+        $memoryPeak = memory_get_peak_usage(true);
+        return [
+            '@timestamp' => (new DateTimeImmutable())->format('c'),
+            'log.level' => 'debug',
+            'id' => $request->getAttribute('request_id', 'unknown'),
+            'event.duration' => $duration,
+            'metrics' => [
+                'memory.usage' => $memoryUsage,
+                'memory.usage.human' => _m_convert($memoryUsage),
+                'memory.peak' => $memoryPeak,
+                'memory.peak.human' => _m_convert(memory_get_peak_usage(true)),
+                'load_time.ms' => ceil($duration * 1000),
+                'load_time.s' => number_format($duration, 3),
+            ],
+            'http.request' => [
+                'method' => $request->getMethod(),
+                'url' => $request->getUri()->__toString(),
+                'path' => $request->getUri()->getPath(),
+                'body' => (string)$request->getBody(),
+                'headers' => $request->getHeaders(),
+                'query' => $request->getQueryParams(),
+                'post' => $request->getParsedBody() ?? [],
+                'cookies' => $request->getCookieParams(),
+                'protocol' => $request->getProtocolVersion(),
+                'server' => $request->getServerParams(),
+            ],
+        ];
+    }
+}

+ 73 - 0
src/Dependency.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace Michel\Framework\Core;
+
+use Michel\Package\PackageInterface;
+
+final class Dependency
+{
+
+    private BaseKernel $baseKernel;
+
+    public function __construct(BaseKernel $baseKernel)
+    {
+        $this->baseKernel = $baseKernel;
+    }
+    public function load(): array
+    {
+        $services = $this->loadConfigurationIfExists('services.php');
+        $parameters = $this->loadParameters('parameters.php');
+        $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->baseKernel->getEnv(), $envs)) {
+                continue;
+            }
+            $packages[] = new $packageName();
+        }
+        return $packages;
+    }
+
+    private function loadConfigurationIfExists(string $fileName): array
+    {
+        return $this->baseKernel->loadConfigurationIfExists($fileName);
+    }
+
+    private function loadParameters(string $fileName): array
+    {
+        $parameters = $this->loadConfigurationIfExists($fileName);
+
+        $parameters['michel.environment'] = $this->baseKernel->getEnv();
+        $parameters['michel.debug'] = $this->baseKernel->isDebug();
+        $parameters['michel.project_dir'] = $this->baseKernel->getProjectDir();
+        $parameters['michel.cache_dir'] = $this->baseKernel->getCacheDir();
+        $parameters['michel.logs_dir'] = $this->baseKernel->getLogDir();
+        $parameters['michel.config_dir'] = $this->baseKernel->getConfigDir();
+        $parameters['michel.public_dir'] = $this->baseKernel->getPublicDir();
+        $parameters['michel.current_cache'] = $this->baseKernel->getEnv() === 'dev' ? null : $this->baseKernel->getCacheDir();
+
+        return $parameters;
+    }
+}

+ 52 - 0
src/ErrorHandler/ErrorHandler.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace Michel\Framework\Core\ErrorHandler;
+
+use ErrorException;
+use function in_array;
+use function set_error_handler;
+use const E_DEPRECATED;
+use const E_USER_DEPRECATED;
+
+final class ErrorHandler
+{
+    private array $deprecations = [];
+
+    public static function register(): self
+    {
+        \error_reporting(E_ALL);
+        ini_set("display_errors", '0');
+        ini_set("display_startup_errors", '0');
+        ini_set('html_errors', (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') ? '0' : '1');
+
+        $handler = new self();
+        set_error_handler($handler);
+        return $handler;
+    }
+
+    public function __invoke(int $level, string $message, ?string $file = null, ?int $line = null): void
+    {
+        if (!error_reporting()) {
+            return;
+        }
+        if (in_array($level, [E_USER_DEPRECATED, E_DEPRECATED])) {
+            $this->deprecations[] = ['level' => $level, 'file' => $file, ' line' => $line, 'message' => $message];
+            return;
+        }
+
+        throw new ErrorException($message, 0, $level, $file, $line);
+    }
+
+    public function clean(): void
+    {
+        restore_error_handler();
+    }
+
+    /**
+     * @return array
+     */
+    public function getDeprecations(): array
+    {
+        return $this->deprecations;
+    }
+}

+ 88 - 0
src/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace Michel\Framework\Core\ErrorHandler\ErrorRenderer;
+
+use InvalidArgumentException;
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface;
+use Michel\Framework\Core\Http\Exception\HttpExceptionInterface;
+use function dirname;
+use function extract;
+use function file_exists;
+use function ob_get_clean;
+use function ob_start;
+use function sprintf;
+
+final class HtmlErrorRenderer
+{
+    private ResponseFactoryInterface $responseFactory;
+    private bool $debug;
+    private ?string $templateDir;
+
+    public function __construct(ResponseFactoryInterface $responseFactory, bool $debug = false, ?string $templateDir = null)
+    {
+        $this->responseFactory = $responseFactory;
+        $this->debug = $debug;
+
+        if (is_string($templateDir)) {
+            if (!file_exists($templateDir) || !is_dir($templateDir)) {
+                throw new InvalidArgumentException(sprintf('%s does not exist', $templateDir));
+            }
+            $this->templateDir = rtrim($templateDir, '/');
+        }
+    }
+
+    public function __invoke(HttpExceptionInterface $exception): ResponseInterface
+    {
+        $response = $this->responseFactory->createResponse($exception->getStatusCode());
+        if ($this->isDebug() === false) {
+            $template = $this->findTemplate($exception->getStatusCode());
+            if ($template !== null) {
+                $response->getBody()->write($this->include($template, [
+                    'exception' => $exception,
+                ]));
+            }
+            return $response;
+        }
+
+
+        $template = filepath_join(dirname(__DIR__), '/../../resources/views/error.html.php');
+        $response->getBody()->write($this->include($template, [
+            'e' => $exception,
+            "server" => $_SERVER,
+            "env" => $_ENV
+
+        ]));
+        return $response;
+    }
+
+    private function include(string $template, array $context = []): string
+    {
+        if (!file_exists($template)) {
+            throw new InvalidArgumentException(sprintf('%s does not exist', $template));
+        }
+        
+        extract($context);
+        ob_start();
+        include($template);
+
+        return trim(ob_get_clean());
+    }
+    
+    public function isDebug(): bool
+    {
+        return $this->debug;
+    }
+
+    private function findTemplate(int $statusCode): ?string
+    {
+        $template = filepath_join($this->templateDir,sprintf('error%s.html.php', $statusCode ));
+        if (file_exists($template)) {
+            return $template;
+        }
+
+        $template = filepath_join( $this->templateDir, 'error.html.php');
+
+        return file_exists($template) ? $template : null;
+    }
+}

+ 45 - 0
src/ErrorHandler/ErrorRenderer/JsonErrorRenderer.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace Michel\Framework\Core\ErrorHandler\ErrorRenderer;
+
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface;
+use Michel\Framework\Core\Http\Exception\HttpExceptionInterface;
+use function json_encode;
+
+final class JsonErrorRenderer
+{
+    private ResponseFactoryInterface $responseFactory;
+    private bool $debug;
+
+    public function __construct(ResponseFactoryInterface $responseFactory, bool $debug = false)
+    {
+        $this->responseFactory = $responseFactory;
+        $this->debug = $debug;
+    }
+
+    public function __invoke(HttpExceptionInterface $exception): ResponseInterface
+    {
+        $response = $this->responseFactory->createResponse($exception->getStatusCode());
+        $data = [
+            'status' => $exception->getStatusCode(),
+            'detail' => $this->isDebug() ? $exception->getMessage() : $exception->getDefaultMessage()
+        ];
+
+        $e = $exception->getPrevious() ?: $exception;
+        if ($this->isDebug() === true) {
+            $data['debug']['class'] = get_class($e);
+            $data['debug']['file'] = $e->getFile();
+            $data['debug']['line'] = $e->getLine();
+            $data['debug']['trace'] = array_merge($e->getTrace(), $exception->getTrace());
+        }
+
+        $response->getBody()->write(json_encode($data));
+        return $response->withHeader('Content-Type', 'application/json');
+    }
+
+    public function isDebug(): bool
+    {
+        return $this->debug;
+    }
+}

+ 63 - 0
src/ErrorHandler/ExceptionHandler.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace Michel\Framework\Core\ErrorHandler;
+
+use Michel\Framework\Core\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
+use Michel\Framework\Core\ErrorHandler\ErrorRenderer\JsonErrorRenderer;
+use Michel\Framework\Core\Http\Exception\HttpException;
+use Michel\Framework\Core\Http\Exception\HttpExceptionInterface;
+use Michel\Resolver\Option;
+use Michel\Resolver\OptionsResolver;
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Throwable;
+
+class ExceptionHandler
+{
+    private ResponseFactoryInterface $responseFactory;
+    private array $options;
+
+    public function __construct(ResponseFactoryInterface $responseFactory, array $options = [])
+    {
+        $this->responseFactory = $responseFactory;
+        $resolver = (new OptionsResolver(
+            [
+                Option::bool("debug", false),
+                Option::mixed("json_response", new JsonErrorRenderer($this->responseFactory, $options['debug']))
+                    ->validator(static function ($value) {
+                        return is_callable($value);
+                    }),
+                Option::mixed("html_response", new HtmlErrorRenderer($this->responseFactory, $options['debug']))
+                    ->validator(static function ($value) {
+                        return is_callable($value);
+                    }),
+            ]
+        ));
+        $this->options = $resolver->resolve($options);
+    }
+
+    public function render(ServerRequestInterface $request, Throwable $exception): ResponseInterface
+    {
+        if (!$exception instanceof HttpExceptionInterface) {
+            $exception = new HttpException(500, $exception->getMessage(), $exception->getCode(), $exception);
+        }
+
+        if ($request->getHeaderLine('accept') === 'application/json') {
+            return $this->renderJsonResponse($exception);
+        }
+        return $this->renderHtmlResponse($exception);
+    }
+
+    protected function renderJsonResponse(HttpExceptionInterface $exception): ResponseInterface
+    {
+        $renderer = $this->options['json_response'];
+        return $renderer($exception);
+    }
+
+    protected function renderHtmlResponse(HttpExceptionInterface $exception): ResponseInterface
+    {
+        $renderer = $this->options['html_response'];
+        return $renderer($exception);
+    }
+}

+ 69 - 0
src/Handler/RequestHandler.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace Michel\Framework\Core\Handler;
+
+use LogicException;
+use Michel\Framework\Core\Debug\DebugDataCollector;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use Throwable;
+use function current;
+use function is_string;
+use function next;
+
+final class RequestHandler implements RequestHandlerInterface
+{
+    private ContainerInterface $container;
+    /**
+     * @var array<MiddlewareInterface, string>
+     */
+    private array $middlewareCollection;
+    private ?\Closure $then;
+
+    public function __construct(ContainerInterface $container, array $middlewareCollection, \Closure $then = null)
+    {
+        $this->container = $container;
+        $this->middlewareCollection = $middlewareCollection;
+        $this->then = $then;
+    }
+
+    /**
+     * @param ServerRequestInterface $request
+     * @return ResponseInterface
+     * @throws Throwable
+     */
+    public function handle(ServerRequestInterface $request): ResponseInterface
+    {
+        $middleware = current($this->middlewareCollection);
+        next($this->middlewareCollection);
+        if ($middleware === false) {
+            $then = $this->then;
+            if ($then instanceof \Closure) {
+                return $then($request);
+            }
+            throw new LogicException('The Middleware must return an instance of Psr\Http\Message\ResponseInterface.');
+        }
+
+        if (is_string($middleware)) {
+            $middleware = $this->container->get($middleware);
+        }
+
+        if (!$middleware instanceof MiddlewareInterface) {
+            throw new LogicException(
+                sprintf(
+                    'The Middleware must be an instance of Psr\Http\Server\MiddlewareInterface, "%s" given.',
+                    is_object($middleware) ? get_class($middleware) : gettype($middleware)
+                )
+            );
+        }
+        $debugCollector = $request->getAttribute('debug_collector');
+        if ($debugCollector instanceof DebugDataCollector) {
+            $debugCollector->push('middlewares_executed', get_class($middleware));
+        }
+
+        return $middleware->process($request, $this);
+    }
+}

+ 27 - 0
src/Helper/IpHelper.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace Michel\Framework\Core\Helper;
+
+use Psr\Http\Message\ServerRequestInterface;
+
+final class IpHelper
+{
+    public static function getIpFromRequest(ServerRequestInterface $request): string
+    {
+        $serverParams = $request->getServerParams();
+        if (array_key_exists('HTTP_CLIENT_IP', $serverParams)) {
+            return $serverParams['HTTP_CLIENT_IP'];
+        }
+
+        if (array_key_exists('HTTP_X_FORWARDED_FOR', $serverParams)) {
+            return $serverParams['HTTP_X_FORWARDED_FOR'];
+        }
+
+        if (array_key_exists('REMOTE_ADDR', $serverParams)) {
+            return $serverParams['REMOTE_ADDR'];
+        }
+
+        return '127.0.0.1';
+    }
+
+}

+ 18 - 0
src/Http/Exception/BadRequestException.php

@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Http\Exception;
+
+/**
+ * @author Michel.F 
+ */
+class BadRequestException extends HttpException
+{
+    protected static ?string $defaultMessage = 'Bad Request';
+
+    public function __construct(?string $message = null, int $code = 0, \Throwable $previous = null)
+    {
+        parent::__construct(400, $message, $code, $previous);
+    }
+}

+ 18 - 0
src/Http/Exception/ForbiddenException.php

@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Http\Exception;
+
+/**
+ * @author Michel.F 
+ */
+class ForbiddenException extends HttpException
+{
+    protected static ?string $defaultMessage = 'Access Denied';
+
+    public function __construct(?string $message = null, int $code = 0, \Throwable $previous = null)
+    {
+        parent::__construct(403, $message, $code, $previous);
+    }
+}

+ 37 - 0
src/Http/Exception/HttpException.php

@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Http\Exception;
+
+/**
+ * @author Michel.F 
+ */
+class HttpException extends \Exception implements HttpExceptionInterface
+{
+    protected static ?string $defaultMessage = 'An error occurred . Please try again later.';
+    private int $statusCode;
+
+    public function __construct(int $statusCode, ?string $message = null, int $code = 0, \Throwable $previous = null)
+    {
+        if ($message === null) {
+            $message = static::$defaultMessage;
+        }
+
+        $this->statusCode = $statusCode;
+        parent::__construct($message, $code, $previous);
+    }
+
+    /**
+     * @return int HTTP status code
+     */
+    public function getStatusCode(): int
+    {
+        return $this->statusCode;
+    }
+
+    public function getDefaultMessage(): string
+    {
+        return static::$defaultMessage;
+    }
+}

+ 25 - 0
src/Http/Exception/HttpExceptionInterface.php

@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Http\Exception;
+
+/**
+ * @author Michel.F 
+ */
+interface HttpExceptionInterface extends \Throwable
+{
+    /**
+     * Returns the status code.
+     *
+     * @return int An HTTP response status code
+     */
+    public function getStatusCode(): int;
+
+    /**
+     * Returns the default message status.
+     *
+     * @return string
+     */
+    public function getDefaultMessage(): string;
+}

+ 18 - 0
src/Http/Exception/MethodNotAllowedException.php

@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Http\Exception;
+
+/**
+ * @author Michel.F 
+ */
+class MethodNotAllowedException extends HttpException
+{
+    protected static ?string $defaultMessage = 'Method Not Allowed';
+
+    public function __construct(?string $message = null, int $code = 0, \Throwable $previous = null)
+    {
+        parent::__construct(405, $message, $code, $previous);
+    }
+}

+ 18 - 0
src/Http/Exception/NotFoundException.php

@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Http\Exception;
+
+/**
+ * @author Michel.F 
+ */
+class NotFoundException extends HttpException
+{
+    protected static ?string $defaultMessage = 'Not Found';
+
+    public function __construct(?string $message = null, int $code = 0, \Throwable $previous = null)
+    {
+        parent::__construct(404, $message, $code, $previous);
+    }
+}

+ 18 - 0
src/Http/Exception/UnauthorizedException.php

@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Http\Exception;
+
+/**
+ * @author Michel.F 
+ */
+class UnauthorizedException extends HttpException
+{
+    protected static ?string $defaultMessage = 'Unauthorized';
+
+    public function __construct(?string $message = null, int $code = 0, \Throwable $previous = null)
+    {
+        parent::__construct(401, $message, $code, $previous);
+    }
+}

+ 123 - 0
src/Middlewares/ControllerMiddleware.php

@@ -0,0 +1,123 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Middlewares;
+
+use BadMethodCallException;
+use LogicException;
+use Michel\Framework\Core\Debug\DebugDataCollector;
+use Michel\Route;
+use Michel\RouterMiddleware;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use Michel\Framework\Core\Controller\Controller;
+use Michel\Framework\Core\Handler\RequestHandler;
+use function array_merge;
+use function array_values;
+use function get_class;
+use function is_callable;
+use function method_exists;
+use function sprintf;
+
+/**
+ * @author Michel.F 
+ */
+final class ControllerMiddleware implements MiddlewareInterface
+{
+
+    private ContainerInterface $container;
+
+    /**
+     * RouterMiddleware constructor.
+     * @param ContainerInterface $container
+     */
+    public function __construct(ContainerInterface $container)
+    {
+        $this->container = $container;
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        $controller = $this->resolveController($request);
+        if ($controller instanceof Controller) {
+            $controller->setContainer($this->container);
+            $requestHandler = new RequestHandler(
+                $this->container,
+                $controller->getMiddlewares(),
+                static function (ServerRequestInterface $request) use ($controller) {
+                    return self::callController($request, $controller);
+                }
+            );
+            return $requestHandler->handle($request);
+        }
+
+        return self::callController($request, $controller);
+    }
+
+    private function resolveController(ServerRequestInterface $request): callable
+    {
+        $route = $request->getAttribute(RouterMiddleware::ATTRIBUTE_KEY);
+        if (!$route instanceof Route) {
+            throw new LogicException('Route not found in request., Maybe you forgot to use Michel\RouterMiddleware?');
+        }
+
+        $handler = $route->getHandler();
+        $controller = $handler[0];
+        $action = $handler[1] ?? null;
+
+        if ($controller instanceof \Closure) {
+            throw new LogicException('Closures are not supported as controllers. Route name: '.$route->getName());
+        }
+
+        if (is_string($controller)) {
+            $controller = $this->container->get($controller);
+        }
+
+        $debugCollector = $request->getAttribute('debug_collector');
+        if ($debugCollector instanceof DebugDataCollector) {
+            $debugCollector->add('route_name', $route->getName());
+            $debugCollector->add('controller', sprintf('%s::%s', get_class($controller), $action ?? '__invoke'));
+        }
+
+        if (is_callable($controller) && $action === null) {
+            return $controller;
+        }
+
+        if (method_exists($controller, $action ?? '') === false) {
+            throw new BadMethodCallException(
+                $action === null
+                    ? sprintf('Please use a Method on class %s.', get_class($controller))
+                    : sprintf('Method "%s" on class %s does not exist.', $action, get_class($controller))
+            );
+        }
+        return [$controller, $action];
+    }
+
+    private static function getArguments(ServerRequestInterface $request): array
+    {
+        $route = $request->getAttribute(RouterMiddleware::ATTRIBUTE_KEY);
+        if (!$route instanceof Route) {
+            throw new LogicException('Route not found in request., Maybe you forgot to use Michel\RouterMiddleware?');
+        }
+        return array_values($route->getAttributes());
+    }
+
+    private static function callController(ServerRequestInterface $request, $controller): ResponseInterface
+    {
+        $arguments = array_merge([$request], self::getArguments($request));
+        /**
+         * @var ResponseInterface $response
+         */
+        $response = $controller(...$arguments);
+        if (!$response instanceof ResponseInterface) {
+            throw new LogicException(
+                'The controller must return an instance of Psr\Http\Message\ResponseInterface.'
+            );
+        }
+        return $response;
+    }
+}

+ 131 - 0
src/Middlewares/DebugMiddleware.php

@@ -0,0 +1,131 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Michel\Framework\Core\Middlewares;
+
+use Michel\Framework\Core\Debug\DebugDataCollector;
+use Michel\Framework\Core\Debug\RequestProfiler;
+use Michel\Renderer\PurePlate;
+use Michel\Resolver\Option;
+use Michel\Resolver\OptionsResolver;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+final class DebugMiddleware implements MiddlewareInterface
+{
+    protected ?RequestProfiler $requestProfiler = null;
+    private bool $debug = false;
+    private bool $profiler = false;
+    private string $env;
+    private ?string $logDir = null;
+
+    public function __construct(array $options = [])
+    {
+        $optionResolver = new OptionsResolver([
+            Option::bool('debug', false),
+            Option::bool('profiler', false),
+            Option::string('env', 'prod'),
+            Option::string('log_dir', 'prod')->validator(function ($value) {
+                return file_exists($value);
+            }),
+        ]);
+
+        $options = $optionResolver->resolve($options);
+        $this->debug = $options['debug'];
+        $this->profiler = $options['profiler'];
+        $this->env = strtolower($options['env']);
+        $this->logDir = rtrim($options['log_dir'], '/') . '/';
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        if (!$this->debug) {
+            return $handler->handle($request);
+        }
+
+        $this->initializeDevelopmentEnvironment();
+
+        $this->requestProfiler->start($request);
+
+        $response = $handler->handle($request);
+        /**
+         * @var DebugDataCollector $debugCollector
+         */
+        $debugCollector = $request->getAttribute('debug_collector');
+        if ($debugCollector) {
+            $debugCollector->add('response_code', $response->getStatusCode());
+            $this->requestProfiler->withDebugDataCollector($debugCollector);
+        }
+        $requestProfilerData = $this->requestProfiler->stop();
+        if ($this->profiler) {
+            if (strpos($response->getHeaderLine('Content-Type'), 'text/html') !== false) {
+                $renderer = new PurePlate(dirname(__DIR__) . '/../resources/debug');
+                $debugBarHtml = $renderer->render('debugbar.html.php', [
+                    'profiler' => $requestProfilerData,
+                ]);
+
+                $body = $response->getBody();
+                $content = (string)$body;
+                $pos = strripos($content, '</body>');
+                if ($pos !== false) {
+                    $response = $response->withBody(response(substr($content, 0, $pos) .
+                        $debugBarHtml .
+                        substr($content, $pos))->getBody());
+                }
+            } elseif (strpos($response->getHeaderLine('Content-Type'), 'application/json') !== false) {
+                $body = $response->getBody();
+                $content = (string)$body;
+                if (!empty(trim($content))) {
+                    $json = json_decode($content, true);
+                    if (is_array($json)) {
+                        unset($requestProfilerData['http.request']);
+                        $requestProfilerDataFlat = array_dot($requestProfilerData);
+                        foreach ($requestProfilerDataFlat as $key => $value) {
+                            $key = str_replace( '@', '', $key);
+                            $key = str_replace( '__', '', $key);
+                            $key = str_replace( '_', '-', $key);
+                            $key = str_replace( '.', '-', $key);
+                            $response = $response->withAddedHeader(sprintf('X-Debug-%s', $key), $value);
+                        }
+                    }
+                }
+            }
+        }
+
+        $this->log($requestProfilerData, 'debug.log');
+
+        return $response;
+    }
+
+    private function initializeDevelopmentEnvironment(): void
+    {
+        $this->requestProfiler = new RequestProfiler([
+            'environment' => $this->env,
+            'php_version' => PHP_VERSION,
+            'php_extensions' => implode(', ', get_loaded_extensions()),
+            'php_sapi' => php_sapi_name(),
+            'php_memory_limit' => ini_get('memory_limit'),
+            'php_timezone' => date_default_timezone_get(),
+        ]);
+    }
+
+    final protected function log(array $data, string $logFile = null): void
+    {
+        if ($this->logDir === null) {
+            return;
+        }
+
+        if (!is_dir($this->logDir)) {
+            @mkdir($this->logDir, 0777, true);
+        }
+
+        if ($logFile === null) {
+            $logFile = $this->env . '.log';
+        }
+
+        file_put_contents(filepath_join($this->logDir, $logFile), json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL, FILE_APPEND|LOCK_EX);
+    }
+}

+ 33 - 0
src/Middlewares/ForceHttpsMiddleware.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Michel\Framework\Core\Middlewares;
+
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+final class ForceHttpsMiddleware implements MiddlewareInterface
+{
+    private bool $forceHttps;
+    private ResponseFactoryInterface $responseFactory;
+
+    public function __construct(bool $forceHttps, ResponseFactoryInterface $responseFactory)
+    {
+        $this->responseFactory = $responseFactory;
+        $this->forceHttps = $forceHttps;
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        if ($this->forceHttps === true && $request->getUri()->getScheme() !== 'https') {
+            $httpsUrl = sprintf('https://%s%s', $request->getUri()->getAuthority(), $request->getUri()->getPath());
+            $response = $this->responseFactory->createResponse(302);
+            return $response->withHeader('Location', $httpsUrl);
+        }
+        return $handler->handle($request);
+    }
+
+
+}

+ 45 - 0
src/Middlewares/IpRestrictionMiddleware.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace Michel\Framework\Core\Middlewares;
+
+use Michel\Framework\Core\Helper\IpHelper;
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+final class IpRestrictionMiddleware implements MiddlewareInterface
+{
+    private array $allowedIps;
+    private ResponseFactoryInterface $responseFactory;
+
+    public function __construct(array $allowedIps, ResponseFactoryInterface $responseFactory)
+    {
+        $this->allowedIps = $allowedIps;
+        $this->responseFactory = $responseFactory;
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        $ip = IpHelper::getIpFromRequest($request);
+        if (!$this->isIpAllowed($ip)) {
+            return $this->responseFactory->createResponse(403, 'Forbidden');
+        }
+
+        return $handler->handle($request);
+    }
+
+    private function isIpAllowed($ip): bool
+    {
+        if ($this->allowedIps === []) {
+            return true;
+        }
+        foreach ($this->allowedIps as $allowedIp) {
+            if (preg_match($allowedIp, $ip)) {
+                return true;
+            }
+        }
+        return in_array($ip, $this->allowedIps);
+    }
+}

+ 79 - 0
src/Middlewares/MaintenanceMiddleware.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace Michel\Framework\Core\Middlewares;
+
+use Michel\Framework\Core\Helper\IpHelper;
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+final class MaintenanceMiddleware implements MiddlewareInterface
+{
+    private bool $maintenanceMode;
+    private ResponseFactoryInterface $responseFactory;
+
+    private array $allowedIps;
+    private ?\Closure $renderer;
+
+    public function __construct(bool $maintenanceMode, ResponseFactoryInterface $responseFactory,array $allowedIps = [], \Closure $renderer = null)
+    {
+        $this->maintenanceMode = $maintenanceMode;
+        $this->responseFactory = $responseFactory;
+        $this->allowedIps = $allowedIps;
+        $this->renderer = $renderer;
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        if ($this->maintenanceMode === true && !in_array(IpHelper::getIpFromRequest($request), $this->allowedIps)) {
+            $response = $this->responseFactory->createResponse(200);
+
+            if ($this->renderer !== null) {
+                $renderer = $this->renderer;
+                $response->getBody()->write($renderer($request));
+            }else{
+                $response->getBody()->write('
+    <html>
+        <head>
+            <title>Maintenance Mode</title>
+            <style>
+                body {
+                    font-family: Arial, sans-serif;
+                    text-align: center;
+                    background-color: #f8f9fa;
+                    color: #333;
+                    padding: 50px;
+                }
+                h1 {
+                    color: #601D17FF;
+                    font-size: 2.5em;
+                }
+                p {
+                    font-size: 1.2em;
+                    line-height: 1.5;
+                }
+                .container {
+                    max-width: 600px;
+                    margin: 0 auto;
+                }
+            </style>
+        </head>
+        <body>
+            <div class="container">
+                <h1>We\'re Undergoing Maintenance</h1>
+                <p>Our website is currently down for scheduled maintenance to improve your experience. We\'ll be back online shortly.</p>
+                <p>Thank you for your patience.</p>
+            </div>
+        </body>
+    </html>
+');
+            }
+            return $response;
+        }
+
+        return $handler->handle($request);
+    }
+
+}

+ 206 - 0
src/Package/MichelCorePackage.php

@@ -0,0 +1,206 @@
+<?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\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\Route;
+use Michel\Router;
+use Michel\RouterInterface;
+use Michel\RouterMiddleware;
+use Michel\Renderer\PurePlate;
+use Psr\Container\ContainerInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
+use Twig\Environment;
+use Twig\Loader\FilesystemLoader;
+use function getenv;
+
+final class MichelCorePackage implements PackageInterface
+{
+    public function getDefinitions(): array
+    {
+        return [
+            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) {
+                if (class_exists(Environment::class)) {
+                    $loader = new FilesystemLoader($container->get('app.template_dir'));
+                    return new Environment($loader, [
+                        'debug' => $container->get('michel.debug'),
+                        'cache' => $container->get('michel.environment') == 'dev' ? false : $container->get('michel.cache_dir'),
+                    ]);
+                } elseif (class_exists(PurePlate::class)) {
+                    return new PurePlate($container->get('app.template_dir'));
+                }
+
+                throw new LogicException('The "render" requires a Renderer to be available. You can choose between installing "michel/pure-plate" or "twig/twig" depending on your preference.');
+            },
+            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,
+        ];
+    }
+}

+ 128 - 0
src/Routing/ControllerFinder.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace Michel\Framework\Core\Routing;
+
+use Michel\Framework\Core\Controller\Controller;
+
+final class ControllerFinder
+{
+    private array $sources = [];
+    private ?string $cacheDir;
+
+    public function __construct(array $sources, ?string $cacheDir = null)
+    {
+        foreach ($sources as $source) {
+            if (!is_dir($source) && !class_exists($source)) {
+                throw new \InvalidArgumentException(
+                    sprintf(
+                        'The source "%s" does not exist or is not a directory.',
+                        $source
+                    )
+                );
+            }
+            $this->sources[] = $source;
+        }
+        $this->cacheDir = $cacheDir;
+        if ($this->cacheDir && !is_dir($this->cacheDir)) {
+            throw  new \InvalidArgumentException(sprintf(
+                'Cache directory "%s" does not exist',
+                $this->cacheDir
+            ));
+        }
+    }
+
+    public function findControllerClasses(): array
+    {
+        $classes = [];
+        foreach ($this->sources as $source) {
+            if (class_exists($source, true) && is_subclass_of($source, Controller::class)) {
+                $classes[] = $source;
+                continue;
+            }
+
+            $classes = array_merge($classes, $this->findControllerClassesInDir($source));
+        }
+
+        return array_unique($classes);
+    }
+
+
+    private function findControllerClassesInDir(string $directory): array
+    {
+        if ($this->cacheIsEnabled()) {
+            $cacheFile = $this->getCacheFile($directory);
+            if (is_file($cacheFile)) {
+                return require $cacheFile;
+            }
+        }
+
+        $classes = [];
+        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory));
+        foreach ($iterator as $file) {
+            if ($file->isDir() || $file->getExtension() !== 'php') {
+                continue;
+            }
+
+            $className = self::extractNamespaceAndClass($file->getPathname());
+            if ($className && class_exists($className, true) && is_subclass_of($className, Controller::class)) {
+                $classes[] = $className;
+            }
+        }
+
+        $classes = array_values($classes);
+        if ($this->cacheIsEnabled()) {
+            $content = "<?php\n\nreturn " . var_export($classes, true) . ";\n";
+            file_put_contents($this->getCacheFile($directory), $content);
+        }
+        return $classes;
+    }
+
+    private function cacheIsEnabled(): bool
+    {
+        return $this->cacheDir !== null;
+    }
+    private function getCacheFile(string $dir): string
+    {
+        return rtrim($this->cacheDir, '/') . '/' . md5($dir) . '.php';
+    }
+
+    private static function extractNamespaceAndClass(string $filePath): string
+    {
+        if (!file_exists($filePath)) {
+            throw new \InvalidArgumentException('File not found: ' . $filePath);
+        }
+
+        $contents = file_get_contents($filePath);
+        $namespace = '';
+        $class = '';
+        $isExtractingNamespace = false;
+        $isExtractingClass = false;
+
+        foreach (token_get_all($contents) as $token) {
+            if (is_array($token) && $token[0] == T_NAMESPACE) {
+                $isExtractingNamespace = true;
+            }
+
+            if (is_array($token) && $token[0] == T_CLASS) {
+                $isExtractingClass = true;
+            }
+
+            if ($isExtractingNamespace) {
+                if (is_array($token) && in_array($token[0], [T_STRING, T_NS_SEPARATOR,  265 /* T_NAME_QUALIFIED For PHP 8*/])) {
+                    $namespace .= $token[1];
+                } else if ($token === ';') {
+                    $isExtractingNamespace = false;
+                }
+            }
+
+            if ($isExtractingClass) {
+                if (is_array($token) && $token[0] == T_STRING) {
+                    $class = $token[1];
+                    break;
+                }
+            }
+        }
+        return $namespace ? $namespace . '\\' . $class : $class;
+    }
+
+}

+ 4 - 0
tests/.env

@@ -0,0 +1,4 @@
+APP_ENV=dev
+APP_TIMEZONE=Europe/Paris
+APP_LOCALE=fr
+APP_URL=http://localhost

+ 0 - 0
tests/.env.test


+ 2 - 0
tests/.gitignore

@@ -0,0 +1,2 @@
+log/
+cache/

+ 75 - 0
tests/AppTest.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace Test\Michel\Framework\Core;
+
+use InvalidArgumentException;
+use Michel\Framework\Core\App;
+use Michel\UniTester\TestCase;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ServerRequestInterface;
+
+class AppTest extends TestCase
+{
+
+    protected function setUp(): void
+    {
+
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+        $this->testInitWithInValidPath();
+        App::initWithPath(__DIR__ . '/config/framework.php');
+        $this->testCreateServerRequest();
+        $this->testGetResponseFactory();
+        $this->testCreateContainer();
+        $this->testGetCustomEnvironments();
+    }
+
+
+    public function testInitWithInValidPath()
+    {
+        $path = 'path/to/your/options.php';
+        $this->expectException(InvalidArgumentException::class, function () use($path) {
+            App::initWithPath($path);
+        });
+    }
+
+    public function testCreateServerRequest()
+    {
+        $request = App::createServerRequest();
+        $this->assertInstanceOf(ServerRequestInterface::class, $request);
+    }
+
+    public function testGetResponseFactory()
+    {
+        $responseFactory = App::getResponseFactory();
+        $this->assertInstanceOf(ResponseFactoryInterface::class, $responseFactory);
+    }
+
+    public function testCreateContainer()
+    {
+        $definitions = []; // Your container definitions
+        $options = []; // Your container options
+
+        $container = App::createContainer($definitions, $options);
+        $this->assertInstanceOf(ContainerInterface::class, $container);
+
+        $container = App::getContainer();
+        $this->assertInstanceOf(ContainerInterface::class, $container);
+    }
+
+    public function testGetCustomEnvironments()
+    {
+        $environments = App::getCustomEnvironments();
+        foreach ($environments as $environment) {
+            $this->assertTrue(is_string($environment));
+        }
+    }
+}

+ 33 - 0
tests/Controller/SampleControllerTest.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Test\Michel\Framework\Core\Controller;
+
+use Michel\Framework\Core\Controller\Controller;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\StreamInterface;
+use Test\Michel\Framework\Core\Response\ResponseTest;
+
+class SampleControllerTest extends Controller
+{
+    public function __construct(array $middleware)
+    {
+        foreach ($middleware as $item) {
+            $this->middleware($item);
+        }
+    }
+
+    public function __invoke() :ResponseInterface
+    {
+        return new ResponseTest();
+    }
+
+    public function testGet(string $id)
+    {
+        return $this->get($id);
+    }
+
+    public function fakeMethod() :ResponseInterface
+    {
+        return new ResponseTest();
+    }
+}

+ 24 - 0
tests/Controller/UserControllerTest.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace Test\Michel\Framework\Core\Controller;
+
+use Michel\Attribute\Route;
+use Michel\Framework\Core\Controller\Controller;
+use Psr\Http\Message\ResponseInterface;
+use Test\Michel\Framework\Core\Response\ResponseTest;
+
+class UserControllerTest extends Controller
+{
+    public function __construct(array $middleware)
+    {
+        foreach ($middleware as $item) {
+            $this->middleware($item);
+        }
+    }
+
+    #[Route('/users', name: 'users')]
+    public function users() :ResponseInterface
+    {
+        return new ResponseTest();
+    }
+}

+ 65 - 0
tests/ControllerFinderTest.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace Test\Michel\Framework\Core;
+
+use Michel\Framework\Core\Routing\ControllerFinder;
+use Michel\UniTester\TestCase;
+use Test\Michel\Framework\Core\Controller\SampleControllerTest;
+use Test\Michel\Framework\Core\Controller\UserControllerTest;
+
+class ControllerFinderTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+       $this->testFound();
+       $this->testFoundCache();
+    }
+    public function testFound()
+    {
+        if (PHP_VERSION_ID >= 80000) {
+            $controllers = (new ControllerFinder([__DIR__ . '/Controller']))->findControllerClasses();
+            $this->assertCount(2, $controllers);
+        }
+        $this->assertTrue(true);
+    }
+
+    public function testFoundCache()
+    {
+        if (PHP_VERSION_ID >= 80000) {
+            $cacheDir = __DIR__ . '/cache';
+            $targetDir = __DIR__ . '/Controller';
+            if (!is_dir($cacheDir)) {
+                mkdir($cacheDir, 0777, true);
+            }
+            $fileCache = "$cacheDir/" . md5($targetDir) . '.php';
+            if (file_exists($fileCache)) {
+                unlink($fileCache);
+            }
+
+            $this->assertFalse(file_exists($fileCache));
+            $controllers = (new ControllerFinder([$targetDir], $cacheDir))->findControllerClasses();
+            $this->assertCount(2, $controllers);
+            $this->assertTrue(file_exists($fileCache));
+            $this->assertEquals([
+                SampleControllerTest::class,
+                UserControllerTest::class,
+            ], require $fileCache);
+
+            $controllers = (new ControllerFinder([$targetDir], $cacheDir))->findControllerClasses();
+            $this->assertCount(2, $controllers);
+            unlink($fileCache);
+            rmdir($cacheDir);
+        }
+        $this->assertTrue(true);
+    }
+}

+ 78 - 0
tests/ControllerMiddlewareTest.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace Test\Michel\Framework\Core;
+
+use Michel\Framework\Core\Middlewares\ControllerMiddleware;
+use Michel\Route;
+use Michel\RouterMiddleware;
+use Michel\UniTester\TestCase;
+use Test\Michel\Framework\Core\Mock\ContainerMock;
+use Test\Michel\Framework\Core\Mock\RequestHandlerMock;
+use Test\Michel\Framework\Core\Mock\ServerRequestMock;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use Test\Michel\Framework\Core\Controller\SampleControllerTest;
+
+class ControllerMiddlewareTest extends TestCase
+{
+
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+        $this->testProcessWithCallableController();
+        $this->testProcessWithControllerMethod();
+        $this->testProcessWithControllerMethodNotExist();
+    }
+    public function testProcessWithCallableController()
+    {
+        $container = new ContainerMock();
+        $controllerMiddleware = new ControllerMiddleware($container);
+
+        $request = new ServerRequestMock([
+            RouterMiddleware::ATTRIBUTE_KEY => new Route('example', '/example', [new SampleControllerTest([])])
+        ]);
+
+        $handler = new RequestHandlerMock();
+        $response = $controllerMiddleware->process($request, $handler);
+        $this->assertInstanceOf(ResponseInterface::class, $response);
+    }
+
+    public function testProcessWithControllerMethod()
+    {
+        $response = $this->testProcessWithController('fakeMethod');
+        $this->assertInstanceOf(ResponseInterface::class, $response);
+    }
+
+    public function testProcessWithControllerMethodNotExist()
+    {
+        $this->expectException(\BadMethodCallException::class, function () {
+            $this->testProcessWithController('fakeMethodNotExist');
+        });
+    }
+
+    private function testProcessWithController(string $controllerMethodName): ResponseInterface
+    {
+        $controllerClassName = SampleControllerTest::class;
+        $container = new ContainerMock([
+            $controllerClassName => new SampleControllerTest([])
+        ]);
+        $controllerMiddleware = new ControllerMiddleware($container);
+
+        $request = new ServerRequestMock([
+            RouterMiddleware::ATTRIBUTE_KEY => new Route('example', '/example', [$controllerClassName, $controllerMethodName])
+        ]);
+
+        return $controllerMiddleware->process($request, new RequestHandlerMock());
+    }
+}

+ 55 - 0
tests/ControllerTest.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace Test\Michel\Framework\Core;
+
+use Michel\UniTester\TestCase;
+use Test\Michel\Framework\Core\Mock\ContainerMock;
+use Test\Michel\Framework\Core\Mock\MiddlewareMock;
+use Psr\Http\Server\MiddlewareInterface;
+use Test\Michel\Framework\Core\Controller\SampleControllerTest;
+
+class ControllerTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+        $this->testMiddleware();
+        $this->testInvalidMiddleware();
+        $this->testGet();
+    }
+    public function testMiddleware()
+    {
+        $middleware = new MiddlewareMock();
+        $controller = new SampleControllerTest([$middleware]);
+        $middlewares = $controller->getMiddlewares();
+        $this->assertInstanceOf(MiddlewareInterface::class, $middlewares[0]);
+    }
+
+    public function testInvalidMiddleware()
+    {
+        $this->expectException(\InvalidArgumentException::class, function () {
+            $invalidMiddleware = 'InvalidMiddlewareClass';
+            new SampleControllerTest([$invalidMiddleware]);
+        });
+
+    }
+
+    public function testGet()
+    {
+        $controller = new SampleControllerTest([]);
+        $container = new ContainerMock([
+            'service_id' => 'service_instance'
+        ]);
+        $controller->setContainer($container);
+        $this->assertEquals('service_instance', $controller->testGet('service_id'));
+    }
+}

+ 41 - 0
tests/ErrorHandlerTest.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace Test\Michel\Framework\Core;
+
+use Michel\Framework\Core\ErrorHandler\ErrorHandler;
+use Michel\UniTester\TestCase;
+
+class ErrorHandlerTest extends TestCase
+{
+
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+       $this->testDeprecationErrors();
+    }
+    public function testDeprecationErrors()
+    {
+        $errorHandler = new ErrorHandler();
+        $errorHandler->__invoke(E_USER_DEPRECATED, 'This is a deprecated error',__FILE__);
+
+        $deprecations = $errorHandler->getDeprecations();
+
+        $this->assertCount(1, $deprecations);
+
+        $deprecation = $deprecations[0];
+        $this->assertArrayHasKey('level', $deprecation);
+        $this->assertEquals(E_USER_DEPRECATED, $deprecation['level']);
+        $this->assertArrayHasKey('message', $deprecation);
+        $this->assertEquals('This is a deprecated error', $deprecation['message']);
+    }
+
+}

+ 51 - 0
tests/Kernel/SampleKernelTest.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace Test\Michel\Framework\Core\Kernel;
+
+use Michel\Framework\Core\BaseKernel;
+
+class  SampleKernelTest extends BaseKernel
+{
+    private string $envFile;
+
+    public function __construct(string $envFile)
+    {
+        $this->envFile = $envFile;
+        parent::__construct();
+    }
+
+    public function getProjectDir(): string
+    {
+       return dirname(__DIR__);
+    }
+
+    public function getCacheDir(): string
+    {
+        return filepath_join($this->getProjectDir(),'cache');
+    }
+
+    public function getLogDir(): string
+    {
+        return filepath_join($this->getProjectDir(),'log');
+    }
+
+    public function getConfigDir(): string
+    {
+        return filepath_join($this->getProjectDir(),'config');
+    }
+
+    public function getPublicDir(): string
+    {
+        return '';
+    }
+
+    public function getEnvFile(): string
+    {
+        return filepath_join( $this->getProjectDir(), $this->envFile);
+    }
+
+    protected function afterBoot(): void
+    {
+        // TODO: Implement afterBoot() method.
+    }
+}

+ 120 - 0
tests/KernelTest.php

@@ -0,0 +1,120 @@
+<?php
+
+namespace Test\Michel\Framework\Core;
+
+use Michel\Framework\Core\BaseKernel;
+use Michel\UniTester\TestCase;
+use Psr\Container\ContainerInterface;
+use Test\Michel\Framework\Core\Kernel\SampleKernelTest;
+use Test\Michel\Framework\Core\Package\MyPackageTest;
+
+class KernelTest extends TestCase
+{
+
+    protected function setUp(): void
+    {
+        $this->initEnv();
+    }
+
+    protected function execute(): void
+    {
+        $this->testLoadKernel();
+        $this->initEnv();
+
+        $this->testLoadConfigurationIfExists();
+        $this->initEnv();
+
+        $this->testDefaultValue();
+        $this->initEnv();
+
+        $this->testKernelContainer();
+    }
+
+    protected function tearDown(): void
+    {
+        $this->initEnv();
+    }
+    private function initEnv(): void
+    {
+        unset($_ENV['APP_ENV']);
+        unset($_SERVER['APP_ENV']);
+        unset($_ENV['APP_TIMEZONE']);
+        unset($_SERVER['APP_TIMEZONE']);
+        unset($_ENV['APP_LOCALE']);
+        unset($_SERVER['APP_LOCALE']);
+        unset($_ENV['APP_URL']);
+        unset($_SERVER['APP_URL']);
+
+        putenv('APP_ENV');
+        putenv('APP_TIMEZONE');
+        putenv('APP_LOCALE');
+        putenv('APP_URL');
+        putenv('APP_DEBUG');
+
+
+        date_default_timezone_set('UTC');
+    }
+    public function testLoadKernel()
+    {
+        $baseKernel = new SampleKernelTest('.env');
+
+        $this->assertEquals('dev', $baseKernel->getEnv());
+        $this->assertEquals(true, $baseKernel->isDebug());
+        $this->assertEquals('dev', getenv('APP_ENV'));
+        $this->assertEquals(0, getenv('APP_DEBUG'));
+        $this->assertEquals('Europe/Paris', getenv('APP_TIMEZONE'));
+        $this->assertEquals('fr', getenv('APP_LOCALE'));
+        $this->assertEquals('http://localhost', getenv('APP_URL'));
+        $this->assertEquals('Europe/Paris', date_default_timezone_get());
+    }
+
+    public function testLoadConfigurationIfExists()
+    {
+        $baseKernel = new SampleKernelTest('.env');
+        $this->assertEquals([], $baseKernel->loadConfigurationIfExists('test.php'));
+    }
+
+    public function testDefaultValue()
+    {
+        $baseKernel = new SampleKernelTest('.env.test');
+        $this->assertEquals('prod', $baseKernel->getEnv());
+        $this->assertEquals('prod', getenv('APP_ENV'));
+        $this->assertEquals('UTC', getenv('APP_TIMEZONE'));
+        $this->assertEquals('en', getenv('APP_LOCALE'));
+        $this->assertFalse(getenv('APP_URL'));
+        $this->assertEquals('UTC', date_default_timezone_get());
+    }
+
+    public function testKernelContainer()
+    {
+        $baseKernel = new SampleKernelTest('.env');
+        $container = $baseKernel->getContainer();
+        $this->assertInstanceOf(ContainerInterface::class, $baseKernel->getContainer());
+
+        $packages = $container->get('michel.packages');
+        $this->assertTrue(is_array($packages));
+        $this->assertInstanceOf(MyPackageTest::class, $packages[0]);
+
+        $this->assertTrue(is_array($container->get('michel.middleware')));
+        $this->assertTrue(is_array($container->get('michel.commands')));
+        $this->assertTrue(is_array($container->get('michel.listeners')));
+        $this->assertTrue(is_array($container->get('michel.routes')));
+        if (PHP_VERSION_ID >= 80000) {
+            $this->assertCount(3, $container->get('michel.routes'));
+        }else {
+            $this->assertCount(2, $container->get('michel.routes'));
+        }
+        $this->assertTrue(is_array($container->get('michel.controllers')));
+        $this->assertCount(2, $container->get('michel.controllers'));
+        $this->assertTrue(is_array($container->get('michel.services_ids')));
+        $this->assertEquals($baseKernel->getEnv(), $container->get('michel.environment'));
+        $this->assertEquals($baseKernel->getEnv() === 'dev', $container->get('michel.debug'));
+        $this->assertEquals($baseKernel->getProjectDir(), $container->get('michel.project_dir'));
+        $this->assertEquals($baseKernel->getCacheDir(), $container->get('michel.cache_dir'));
+        $this->assertEquals($baseKernel->getLogDir(), $container->get('michel.logs_dir'));
+        $this->assertEquals($baseKernel->getConfigDir(), $container->get('michel.config_dir'));
+        $this->assertEquals($baseKernel->getPublicDir(), $container->get('michel.public_dir'));
+        $this->assertInstanceOf(BaseKernel::class, $container->get(BaseKernel::class));
+    }
+
+}

+ 96 - 0
tests/MichelCorePackageTest.php

@@ -0,0 +1,96 @@
+<?php
+
+namespace Test\Michel\Framework\Core;
+
+use Michel\Console\CommandRunner;
+use Michel\Framework\Core\ErrorHandler\ExceptionHandler;
+use Michel\Framework\Core\Package\MichelCorePackage;
+use Michel\RouterInterface;
+use Michel\RouterMiddleware;
+use Michel\UniTester\TestCase;
+use Psr\EventDispatcher\EventDispatcherInterface;
+
+class MichelCorePackageTest extends TestCase
+{
+
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+        $this->testGetDefinitions();
+        $this->testGetParameters();
+        $this->testGetRoutes();
+        $this->testGetListeners();
+        $this->testGetCommands();
+    }
+
+    public function testGetDefinitions()
+    {
+        $package = new MichelCorePackage();
+
+        $definitions = $package->getDefinitions();
+        $this->assertNotEmpty($definitions);
+
+        $this->assertArrayHasKey(EventDispatcherInterface::class, $definitions);
+        $this->assertTrue(is_callable($definitions[EventDispatcherInterface::class]));
+
+        $this->assertArrayHasKey(RouterInterface::class, $definitions);
+        $this->assertTrue(is_callable($definitions[RouterInterface::class]));
+
+        $this->assertArrayHasKey('render', $definitions);
+        $this->assertTrue(is_callable($definitions['render']));
+
+        $this->assertArrayHasKey(CommandRunner::class, $definitions);
+        $this->assertTrue(is_callable($definitions[CommandRunner::class]));
+
+        $this->assertArrayHasKey(RouterMiddleware::class, $definitions);
+        $this->assertTrue(is_callable($definitions[RouterMiddleware::class]));
+
+        $this->assertArrayHasKey(ExceptionHandler::class, $definitions);
+        $this->assertTrue(is_callable($definitions[ExceptionHandler::class]));
+    }
+
+    public function testGetParameters()
+    {
+        $package = new MichelCorePackage();
+
+        $parameters = $package->getParameters();
+
+        $this->assertNotEmpty($parameters);
+        $this->assertArrayHasKey('app.url', $parameters);
+        $this->assertArrayHasKey('app.locale', $parameters);
+        $this->assertArrayHasKey('app.template_dir', $parameters);
+    }
+
+    public function testGetRoutes()
+    {
+        $package = new MichelCorePackage();
+        $routes = $package->getRoutes();
+        $this->assertEmpty($routes);
+    }
+
+    public function testGetListeners()
+    {
+        $package = new MichelCorePackage();
+        $listeners = $package->getListeners();
+        $this->assertEmpty($listeners);
+    }
+
+    public function testGetCommands()
+    {
+        $package = new MichelCorePackage();
+        $commands = $package->getCommandSources();
+        $this->assertNotEmpty($commands);
+        foreach ($commands as $command) {
+            $this->assertTrue(is_string($command));
+        }
+    }
+}

+ 18 - 0
tests/Middleware/ResponseMiddlewareTest.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Test\Michel\Framework\Core\Middleware;
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use Test\Michel\Framework\Core\Response\ResponseTest;
+
+class ResponseMiddlewareTest implements MiddlewareInterface
+{
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        return new ResponseTest();
+    }
+}

+ 29 - 0
tests/Mock/ContainerMock.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace Test\Michel\Framework\Core\Mock;
+
+use Psr\Container\ContainerInterface;
+
+class ContainerMock implements ContainerInterface
+{
+    private array $definitions;
+    public function __construct(array $definitions = [])
+    {
+        $this->definitions = $definitions;
+    }
+
+    public function get(string $id)
+    {
+        $value = $this->definitions[$id] ?? null;
+        if ($value instanceof \Closure) {
+            return $value($this);
+        }
+        return $value;
+    }
+
+    public function has(string $id): bool
+    {
+        return array_key_exists($id, $this->definitions);
+    }
+
+}

+ 18 - 0
tests/Mock/MiddlewareMock.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Test\Michel\Framework\Core\Mock;
+
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+class MiddlewareMock implements MiddlewareInterface
+{
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        // TODO: Implement process() method.
+    }
+}

+ 17 - 0
tests/Mock/RequestHandlerMock.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Test\Michel\Framework\Core\Mock;
+
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+class RequestHandlerMock implements RequestHandlerInterface
+{
+
+    public function handle(ServerRequestInterface $request): ResponseInterface
+    {
+        // TODO: Implement handle() method.
+    }
+}

+ 82 - 0
tests/Mock/ResponseMock.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace Test\Michel\Framework\Core\Mock;
+
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\MessageInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\StreamInterface;
+
+class ResponseMock implements ResponseInterface
+{
+
+    public function getProtocolVersion(): string
+    {
+        // TODO: Implement getProtocolVersion() method.
+    }
+
+    public function withProtocolVersion(string $version): MessageInterface
+    {
+        // TODO: Implement withProtocolVersion() method.
+    }
+
+    public function getHeaders(): array
+    {
+        // TODO: Implement getHeaders() method.
+    }
+
+    public function hasHeader(string $name): bool
+    {
+        // TODO: Implement hasHeader() method.
+    }
+
+    public function getHeader(string $name): array
+    {
+        // TODO: Implement getHeader() method.
+    }
+
+    public function getHeaderLine(string $name): string
+    {
+        // TODO: Implement getHeaderLine() method.
+    }
+
+    public function withHeader(string $name, $value): MessageInterface
+    {
+        // TODO: Implement withHeader() method.
+    }
+
+    public function withAddedHeader(string $name, $value): MessageInterface
+    {
+        // TODO: Implement withAddedHeader() method.
+    }
+
+    public function withoutHeader(string $name): MessageInterface
+    {
+        // TODO: Implement withoutHeader() method.
+    }
+
+    public function getBody(): StreamInterface
+    {
+        // TODO: Implement getBody() method.
+    }
+
+    public function withBody(StreamInterface $body): MessageInterface
+    {
+        // TODO: Implement withBody() method.
+    }
+
+    public function getStatusCode(): int
+    {
+        // TODO: Implement getStatusCode() method.
+    }
+
+    public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface
+    {
+        // TODO: Implement withStatus() method.
+    }
+
+    public function getReasonPhrase(): string
+    {
+        // TODO: Implement getReasonPhrase() method.
+    }
+}

+ 172 - 0
tests/Mock/ServerRequestMock.php

@@ -0,0 +1,172 @@
+<?php
+
+namespace Test\Michel\Framework\Core\Mock;
+
+use Psr\Http\Message\MessageInterface;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UriInterface;
+
+class ServerRequestMock implements ServerRequestInterface
+{
+
+    public function __construct(
+        array $attributes = []
+    )
+    {
+        $this->attributes = $attributes;
+    }
+
+    public function getServerParams(): array
+    {
+        // TODO: Implement getServerParams() method.
+    }
+
+    public function getCookieParams(): array
+    {
+        // TODO: Implement getCookieParams() method.
+    }
+
+    public function withCookieParams(array $cookies): ServerRequestInterface
+    {
+        // TODO: Implement withCookieParams() method.
+    }
+
+    public function getQueryParams(): array
+    {
+        // TODO: Implement getQueryParams() method.
+    }
+
+    public function withQueryParams(array $query): ServerRequestInterface
+    {
+        // TODO: Implement withQueryParams() method.
+    }
+
+    public function getUploadedFiles(): array
+    {
+        // TODO: Implement getUploadedFiles() method.
+    }
+
+    public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface
+    {
+        // TODO: Implement withUploadedFiles() method.
+    }
+
+    public function getParsedBody()
+    {
+        // TODO: Implement getParsedBody() method.
+    }
+
+    public function withParsedBody($data): ServerRequestInterface
+    {
+        // TODO: Implement withParsedBody() method.
+    }
+
+    public function getAttributes(): array
+    {
+        return $this->attributes;
+    }
+
+    public function getAttribute(string $name, $default = null)
+    {
+        return $this->attributes[$name] ?? $default;
+    }
+
+    public function withAttribute(string $name, $value): ServerRequestInterface
+    {
+        // TODO: Implement withAttribute() method.
+    }
+
+    public function withoutAttribute(string $name): ServerRequestInterface
+    {
+        // TODO: Implement withoutAttribute() method.
+    }
+
+    private array $attributes;
+
+    public function getProtocolVersion(): string
+    {
+        // TODO: Implement getProtocolVersion() method.
+    }
+
+    public function withProtocolVersion(string $version): MessageInterface
+    {
+        // TODO: Implement withProtocolVersion() method.
+    }
+
+    public function getHeaders(): array
+    {
+        return [];
+    }
+
+    public function hasHeader(string $name): bool
+    {
+        // TODO: Implement hasHeader() method.
+    }
+
+    public function getHeader(string $name): array
+    {
+        // TODO: Implement getHeader() method.
+    }
+
+    public function getHeaderLine(string $name): string
+    {
+        // TODO: Implement getHeaderLine() method.
+    }
+
+    public function withHeader(string $name, $value): MessageInterface
+    {
+        // TODO: Implement withHeader() method.
+    }
+
+    public function withAddedHeader(string $name, $value): MessageInterface
+    {
+        // TODO: Implement withAddedHeader() method.
+    }
+
+    public function withoutHeader(string $name): MessageInterface
+    {
+        // TODO: Implement withoutHeader() method.
+    }
+
+    public function getBody(): StreamInterface
+    {
+        // TODO: Implement getBody() method.
+    }
+
+    public function withBody(StreamInterface $body): MessageInterface
+    {
+        // TODO: Implement withBody() method.
+    }
+
+    public function getRequestTarget(): string
+    {
+        // TODO: Implement getRequestTarget() method.
+    }
+
+    public function withRequestTarget(string $requestTarget): RequestInterface
+    {
+        // TODO: Implement withRequestTarget() method.
+    }
+
+    public function getMethod(): string
+    {
+        // TODO: Implement getMethod() method.
+    }
+
+    public function withMethod(string $method): RequestInterface
+    {
+        // TODO: Implement withMethod() method.
+    }
+
+    public function getUri(): UriInterface
+    {
+        // TODO: Implement getUri() method.
+    }
+
+    public function withUri(UriInterface $uri, bool $preserveHost = false): RequestInterface
+    {
+        // TODO: Implement withUri() method.
+    }
+}

+ 63 - 0
tests/Package/MyPackageTest.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace Test\Michel\Framework\Core\Package;
+
+use Michel\Framework\Core\Command\CacheClearCommand;
+use Michel\Framework\Core\Command\MakeCommandCommand;
+use Michel\Package\PackageInterface;
+use Michel\Route;
+use Michel\RouterInterface;
+use Psr\Container\ContainerInterface;
+
+class MyPackageTest implements PackageInterface
+{
+    public function getDefinitions(): array
+    {
+        return [
+            RouterInterface::class => static function (ContainerInterface $container) {
+                return new \stdClass();
+            },
+            'render' => static function (ContainerInterface $container) {
+                return new \stdClass();
+            },
+        ];
+    }
+
+    public function getParameters(): array
+    {
+        return [
+            'app.url' => 'https://example.com',
+            'app.locale' => 'en',
+            'app.template_dir' => '/path/to/templates',
+        ];
+    }
+
+    public function getRoutes(): array
+    {
+        return [
+            new Route('example', '/example', function () {}),
+            new Route('another', '/another', function () {}),
+        ];
+    }
+
+    public function getListeners(): array
+    {
+        return [
+            'App\\Event\\ExampleEvent' => \stdClass::class,
+            'App\\Event\\AnotherEvent' => \stdClass::class,
+        ];
+    }
+
+    public function getCommandSources(): array
+    {
+        return [
+            CacheClearCommand::class,
+            MakeCommandCommand::class,
+        ];
+    }
+
+    public function getControllerSources(): array
+    {
+        return [];
+    }
+}

+ 75 - 0
tests/RequestHandlerTest.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace Test\Michel\Framework\Core;
+
+use Michel\Framework\Core\Handler\RequestHandler;
+use Michel\UniTester\TestCase;
+use Test\Michel\Framework\Core\Mock\ContainerMock;
+use Test\Michel\Framework\Core\Mock\ResponseMock;
+use Test\Michel\Framework\Core\Mock\ServerRequestMock;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Test\Michel\Framework\Core\Middleware\ResponseMiddlewareTest;
+
+class RequestHandlerTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+        $this->testResponseOk();
+        $this->testInvalidMiddleware();
+        $this->testThenArgument();
+    }
+
+    public function testResponseOk()
+    {
+        /**
+         * @var ContainerInterface $container
+         * @var ServerRequestInterface $request
+         */
+        $container = new ContainerMock();
+        $request = new ServerRequestMock();
+        $handler = new RequestHandler($container, [new ResponseMiddlewareTest()]);
+        $this->assertEquals(200, $handler->handle($request)->getStatusCode());
+    }
+
+    public function testInvalidMiddleware()
+    {
+        /**
+         * @var ContainerInterface $container
+         * @var ServerRequestInterface $request
+         */
+        $container = new ContainerMock();
+        $request = new ServerRequestMock();
+        $handler = new RequestHandler($container, [new \stdClass()]);
+        $this->expectException(\LogicException::class, function () use($handler, $request) {
+            $handler->handle($request);
+        });
+    }
+
+    public function testThenArgument()
+    {
+        /**
+         * @var ContainerInterface $container
+         * @var ServerRequestInterface $request
+         */
+        $container = new ContainerMock();
+        $request = new ServerRequestMock();
+        $handler = new RequestHandler($container, [],   function (ServerRequestInterface $request) {
+            return new ResponseMock();
+        });
+        $response = $handler->handle($request);
+        $this->assertInstanceOf(ResponseInterface::class, $response);
+    }
+
+}

+ 79 - 0
tests/Response/ResponseTest.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace Test\Michel\Framework\Core\Response;
+
+use http\Message\Body;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\StreamInterface;
+
+class ResponseTest implements ResponseInterface {
+    public function getProtocolVersion(): string
+    {
+        // TODO: Implement getProtocolVersion() method.
+    }
+
+    public function withProtocolVersion(string $version): \Psr\Http\Message\MessageInterface
+    {
+        // TODO: Implement withProtocolVersion() method.
+    }
+
+    public function getHeaders(): array
+    {
+        // TODO: Implement getHeaders() method.
+    }
+
+    public function hasHeader(string $name): bool
+    {
+        // TODO: Implement hasHeader() method.
+    }
+
+    public function getHeader(string $name): array
+    {
+        // TODO: Implement getHeader() method.
+    }
+
+    public function getHeaderLine(string $name): string
+    {
+        // TODO: Implement getHeaderLine() method.
+    }
+
+    public function withHeader(string $name, $value): \Psr\Http\Message\MessageInterface
+    {
+        // TODO: Implement withHeader() method.
+    }
+
+    public function withAddedHeader(string $name, $value): \Psr\Http\Message\MessageInterface
+    {
+        // TODO: Implement withAddedHeader() method.
+    }
+
+    public function withoutHeader(string $name): \Psr\Http\Message\MessageInterface
+    {
+        // TODO: Implement withoutHeader() method.
+    }
+
+    public function getBody(): StreamInterface
+    {
+        // TODO: Implement getBody() method.
+    }
+
+    public function withBody(StreamInterface $body): \Psr\Http\Message\MessageInterface
+    {
+        // TODO: Implement withBody() method.
+    }
+
+    public function getStatusCode(): int
+    {
+        return 200;
+    }
+
+    public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface
+    {
+        // TODO: Implement withStatus() method.
+    }
+
+    public function getReasonPhrase(): string
+    {
+        // TODO: Implement getReasonPhrase() method.
+    }
+};

+ 5 - 0
tests/config/controllers.php

@@ -0,0 +1,5 @@
+<?php
+
+return [
+    dirname(__DIR__) . '/Controller/',
+];

+ 39 - 0
tests/config/framework.php

@@ -0,0 +1,39 @@
+<?php
+
+
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestFactoryInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Test\Michel\Framework\Core\Mock\ContainerMock;
+use Test\Michel\Framework\Core\Mock\ServerRequestMock;
+use Test\Michel\Framework\Core\Response\ResponseTest;
+
+return [
+    'server_request' => static function (): ServerRequestInterface {
+        return new ServerRequestMock();
+    },
+    'response_factory' => static function (): ResponseFactoryInterface {
+        return new class implements ResponseFactoryInterface {
+            public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
+            {
+                return new ResponseTest();
+            }
+        };
+    },
+    'server_request_factory' => static function (): ServerRequestFactoryInterface {
+
+        return new class implements ServerRequestFactoryInterface {
+            public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
+            {
+                // TODO: Implement createServerRequest() method.
+            }
+        };
+
+    },
+    'container' => static function (array $definitions, array $options): ContainerInterface {
+        return new ContainerMock($definitions);
+    },
+    'custom_environments' => ['test'],
+];

+ 6 - 0
tests/config/middleware.php

@@ -0,0 +1,6 @@
+<?php
+
+return [
+    \Michel\RouterMiddleware::class => [],
+    \Michel\Framework\Core\Middlewares\ControllerMiddleware::class => [],
+];

+ 7 - 0
tests/config/packages.php

@@ -0,0 +1,7 @@
+<?php
+
+use Test\Michel\Framework\Core\Package\MyPackageTest;
+
+return [
+    MyPackageTest::class => ['dev', 'prod']
+];

+ 7 - 0
tests/config/parameters.php

@@ -0,0 +1,7 @@
+<?php
+
+return [
+    'app.url' => 'https://example.com',
+    'app.locale' => 'en',
+    'app.template_dir' => '/path/to/templates',
+];

Some files were not shown because too many files changed in this diff