diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b759eaa..16381b6 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,33 +1,62 @@ # πŸ‘‹ Hey there, fellow contributor -> Thank you for being here and considering a contribution to WebifyCMS, -> and together we can make great software. +> Thank you for being here and considering a contribution to WebifyCMS! +> Together we can make great software. -## Let's Set Up for Development +--- + +## βš–οΈ Developer Certificate of Origin (DCO) + +To keep WebifyCMS open, secure, and legally clean, we use a **Developer Certificate of Origin (DCO)** +instead of a complex contributor agreement. + +By contributing to this project, you certify that you have the right to submit the code +under the project's open-source license. + +### How to Sign Your Work +All you need to do is sign off on your git commits. +You can do this automatically by adding the `-s` flag when you commit: + +```bash +git commit -s -m "Your meaningful commit message" +``` + +This automatically appends a line to your commit message like this: +`Signed-off-by: Your Name ` + +*Note: Pull Requests with unsigned commits cannot be merged. If you forget to sign a commit, +you can fix it using `git commit --amend -s` before pushing.* + +--- + +## πŸ› οΈ Let's Set Up for Development ### Prerequisites -* An environment that can run Docker or Docker Desktop. Please make sure you have installed one. +* An environment capable of running Docker or Docker Desktop. -### Installation +### 1. Installation -Use WebifyCMS [installer](https://github.com/webifycms/installer) package to install WebifyCMS. +Clone the main repository and use the [WebifyCMS Installer](https://github.com/webifycms/installer) +package to initialize the local stack setup. -### Create .env file +### 2. Environment Configuration + +Navigate to your application `app` directory and copy the sample environment file: ```bash cd app && cp .env.sample .env ``` -> **NOTE:** The above command will create a `.env` file from the sample file provided -> in the project root and fill the values in the file according to your needs. -> And make sure the `APP_ENVIRONMENT` is set to 'development' and `APP_DEBUG` -> is set to `false`. +Open the newly created `.env` file and adjust the values for your local setup. +Ensure the environment variables are configured for local debugging: + +* `APP_ENVIRONMENT='development'` +* `APP_DEBUG=true` -> **NOTE:** There are some other default values, and you can change them as needed. -> Add the configured `APP_BASE_URL` value to the host file. +Add your configured `APP_BASE_URL` value to your local system's host file if you are using a custom local domain. -At this point, the expected folder structure will be like this: +At this stage, your local folder structure should match this layout: ``` . @@ -36,29 +65,36 @@ At this point, the expected folder structure will be like this: β”‚ β”œβ”€β”€ ... β”‚ β”œβ”€β”€ .env β”‚ β”œβ”€β”€ .env.sample + β”‚ β”œβ”€β”€ composer.json + β”‚ β”œβ”€β”€ composer.lock + β”‚ β”œβ”€β”€ src/ + β”‚ β”œβ”€β”€ extensions/ + β”‚ β”œβ”€β”€ themes/ + β”‚ β”œβ”€β”€ vendor/ β”‚ └── ... - β”œβ”€β”€ extensions - β”‚ β”œβ”€β”€ ext-base - β”œβ”€β”€ themes - β”‚ β”œβ”€β”€ theme-canvas + β”œβ”€β”€ extensions/ + β”‚ └── ext-base/ + └── themes/ + └── theme-canvas/ ``` -### Install Dependencies +### 3. Install Dependencies + +Switch to your working branches inside the `app` and `extensions` directories, then install the PHP packages: ```bash -# first checkout to the local branch in the 'app' and the 'extensions' php composer install ``` -### Run the Application +### 4. Run the Application -You can use PHP's built-in web server: +You can spin up the application quickly using PHP's built-in development web server: ```bash php -S localhost:8000 -t public/ ``` -The application is now available at `http://localhost:8000`. +The application will now be available in your browser at `http://localhost:8000`. -> **Note:** The built-in server is single-threaded and intended for development -> only. Use a dedicated web server (Nginx, Caddy, etc.) in production. \ No newline at end of file +> πŸ’‘ **Note:** The built-in PHP server is single-threaded and intended strictly for local development. +> Use a dedicated web server like Nginx, Apache, or Caddy for production deployments. \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 1de11b0..7329b32 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -5,7 +5,7 @@ * * @see https://webifycms.com * - * @copyright Copyright (c) 2023 WebifyCMS + * @copyright Copyright (c) 2023 - Present WebifyCMS * @license https://webifycms.com/license * @author Mohammed Shifreen */ @@ -34,7 +34,7 @@ @see https://webifycms.com - @copyright Copyright (c) 2023 WebifyCMS + @copyright Copyright (c) 2023 - Present WebifyCMS @license https://webifycms.com/license @author Mohammed Shifreen HEADER; diff --git a/LICENSE.md b/LICENSE.md index 9b86eb7..8d21a95 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # WebifyCMS License -Copyright © 2023 WebifyCMS () +Copyright Β© 2023–Present WebifyCMS () Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,13 @@ furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +**"WebifyCMS" is a registered trademark of WebifyCMS. Use of the name +"WebifyCMS" in derivative works requires prior written permission.** + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/composer.json b/composer.json index 870866a..73edf92 100644 --- a/composer.json +++ b/composer.json @@ -8,10 +8,12 @@ "cms", "cms project", "php cms", - "yii2 cms", "ddd cms", "php ddd", - "app template" + "app template", + "php app template", + "clean architecture", + "php clean architecture" ], "homepage": "https://www.webifycms.com/", "type": "project", @@ -23,7 +25,7 @@ "prefer-stable": true, "require": { "php": ">=8.4", - "webifycms/ext-base": "dev-main", + "webifycms/ext-base": "^0.1.0-alpha", "oomphinc/composer-installers-extender": "^2.0" }, "require-dev": { @@ -35,10 +37,6 @@ "roave/security-advisories": "dev-latest" }, "repositories": [ - { - "type": "vcs", - "url": "https://github.com/webifycms/ext-base" - }, { "type": "composer", "url": "https://asset-packagist.org" diff --git a/composer.lock b/composer.lock index d68b9d4..04ef3bd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c211c158e684ac6c7b80e420bc5e19ec", + "content-hash": "256a59bea4ba7199bc3acb33f123ff11", "packages": [ { "name": "brick/math", - "version": "0.17.2", + "version": "0.18.0", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "8189e751995f9e15729c1aa2f89fa8f166ffe818" + "reference": "82944324d1c1bdb2c2618e89978d4e2ad78d69ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/8189e751995f9e15729c1aa2f89fa8f166ffe818", - "reference": "8189e751995f9e15729c1aa2f89fa8f166ffe818", + "url": "https://api.github.com/repos/brick/math/zipball/82944324d1c1bdb2c2618e89978d4e2ad78d69ad", + "reference": "82944324d1c1bdb2c2618e89978d4e2ad78d69ad", "shasum": "" }, "require": { @@ -55,7 +55,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.17.2" + "source": "https://github.com/brick/math/tree/0.18.0" }, "funding": [ { @@ -63,7 +63,7 @@ "type": "github" } ], - "time": "2026-05-25T20:34:43+00:00" + "time": "2026-06-14T18:21:03+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -434,7 +434,7 @@ }, { "name": "illuminate/collections", - "version": "v13.15.0", + "version": "v13.17.0", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", @@ -494,7 +494,7 @@ }, { "name": "illuminate/conditionable", - "version": "v13.15.0", + "version": "v13.17.0", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", @@ -540,16 +540,16 @@ }, { "name": "illuminate/container", - "version": "v13.15.0", + "version": "v13.17.0", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", - "reference": "94af2a79236031ba3dfc93548c8cc545d3bfc4b3" + "reference": "50a24585e90cfdede3b241234fb29c803005022c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/container/zipball/94af2a79236031ba3dfc93548c8cc545d3bfc4b3", - "reference": "94af2a79236031ba3dfc93548c8cc545d3bfc4b3", + "url": "https://api.github.com/repos/illuminate/container/zipball/50a24585e90cfdede3b241234fb29c803005022c", + "reference": "50a24585e90cfdede3b241234fb29c803005022c", "shasum": "" }, "require": { @@ -598,20 +598,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-05-28T23:50:37+00:00" + "time": "2026-06-10T13:09:02+00:00" }, { "name": "illuminate/contracts", - "version": "v13.15.0", + "version": "v13.17.0", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "dc805389d17f13821726423bf4fc90d3162bf90d" + "reference": "0b719e8cac0dcdb021e0ddfee5ef65410fe53000" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/dc805389d17f13821726423bf4fc90d3162bf90d", - "reference": "dc805389d17f13821726423bf4fc90d3162bf90d", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/0b719e8cac0dcdb021e0ddfee5ef65410fe53000", + "reference": "0b719e8cac0dcdb021e0ddfee5ef65410fe53000", "shasum": "" }, "require": { @@ -646,24 +646,24 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-05-21T13:18:48+00:00" + "time": "2026-06-22T12:59:19+00:00" }, { "name": "illuminate/database", - "version": "v13.15.0", + "version": "v13.17.0", "source": { "type": "git", "url": "https://github.com/illuminate/database.git", - "reference": "c40698eea15154c4f0e8a6fef7c09b1cbcac6ec8" + "reference": "e9e3901def42739290761e28fab10e0433a1a07b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/database/zipball/c40698eea15154c4f0e8a6fef7c09b1cbcac6ec8", - "reference": "c40698eea15154c4f0e8a6fef7c09b1cbcac6ec8", + "url": "https://api.github.com/repos/illuminate/database/zipball/e9e3901def42739290761e28fab10e0433a1a07b", + "reference": "e9e3901def42739290761e28fab10e0433a1a07b", "shasum": "" }, "require": { - "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17", + "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17 || ^0.18", "ext-pdo": "*", "illuminate/collections": "^13.0", "illuminate/container": "^13.0", @@ -719,11 +719,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-06-08T13:43:48+00:00" + "time": "2026-06-22T14:01:12+00:00" }, { "name": "illuminate/macroable", - "version": "v13.15.0", + "version": "v13.17.0", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -769,16 +769,16 @@ }, { "name": "illuminate/reflection", - "version": "v13.15.0", + "version": "v13.17.0", "source": { "type": "git", "url": "https://github.com/illuminate/reflection.git", - "reference": "4fe1659f068ab2b50131cf906c5d8bba4e34df0c" + "reference": "178cdb5eb08c5369ae6b71518e557bed68e74577" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/reflection/zipball/4fe1659f068ab2b50131cf906c5d8bba4e34df0c", - "reference": "4fe1659f068ab2b50131cf906c5d8bba4e34df0c", + "url": "https://api.github.com/repos/illuminate/reflection/zipball/178cdb5eb08c5369ae6b71518e557bed68e74577", + "reference": "178cdb5eb08c5369ae6b71518e557bed68e74577", "shasum": "" }, "require": { @@ -816,20 +816,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-03-10T20:04:12+00:00" + "time": "2026-06-18T15:47:27+00:00" }, { "name": "illuminate/support", - "version": "v13.15.0", + "version": "v13.17.0", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "1283a6f6e6b6b28e17bb92280a71a832294814c0" + "reference": "0e8c393be2fb6163be35b4a738075bf76444c246" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/1283a6f6e6b6b28e17bb92280a71a832294814c0", - "reference": "1283a6f6e6b6b28e17bb92280a71a832294814c0", + "url": "https://api.github.com/repos/illuminate/support/zipball/0e8c393be2fb6163be35b4a738075bf76444c246", + "reference": "0e8c393be2fb6163be35b4a738075bf76444c246", "shasum": "" }, "require": { @@ -895,7 +895,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-06-08T13:47:24+00:00" + "time": "2026-06-21T21:14:09+00:00" }, { "name": "laminas/laminas-httphandlerrunner", @@ -1279,16 +1279,16 @@ }, { "name": "nesbot/carbon", - "version": "3.11.4", + "version": "3.13.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60" + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60", - "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/40f6618f052df16b545f626fbf9a878e6497d16a", + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a", "shasum": "" }, "require": { @@ -1380,7 +1380,7 @@ "type": "tidelift" } ], - "time": "2026-04-07T09:57:54+00:00" + "time": "2026-06-18T13:49:15+00:00" }, { "name": "nikic/fast-route", @@ -2361,16 +2361,16 @@ }, { "name": "symfony/cache", - "version": "v8.1.0", + "version": "v8.1.1", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "ba62e0ed9ea9bc26142844a891d4a3dfceb24aed" + "reference": "c14decc1b0755b1e8ab6babeef56e1880348e817" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/ba62e0ed9ea9bc26142844a891d4a3dfceb24aed", - "reference": "ba62e0ed9ea9bc26142844a891d4a3dfceb24aed", + "url": "https://api.github.com/repos/symfony/cache/zipball/c14decc1b0755b1e8ab6babeef56e1880348e817", + "reference": "c14decc1b0755b1e8ab6babeef56e1880348e817", "shasum": "" }, "require": { @@ -2436,7 +2436,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v8.1.0" + "source": "https://github.com/symfony/cache/tree/v8.1.1" }, "funding": [ { @@ -2456,20 +2456,20 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-06-17T15:04:37+00:00" }, { "name": "symfony/cache-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "225e8a254166bd3442e370c6f50145465db63831" + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/225e8a254166bd3442e370c6f50145465db63831", - "reference": "225e8a254166bd3442e370c6f50145465db63831", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/9789738bc19af1106dc54d6afba9a0b467516cf2", + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2", "shasum": "" }, "require": { @@ -2516,7 +2516,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/cache-contracts/tree/v3.7.1" }, "funding": [ { @@ -2536,7 +2536,7 @@ "type": "tidelift" } ], - "time": "2026-05-05T15:33:14+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/clock", @@ -2617,16 +2617,16 @@ }, { "name": "symfony/console", - "version": "v8.1.0", + "version": "v8.1.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "f5a856c6ecb56b3c21ed94a5b7bf940d857d110a" + "reference": "b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/f5a856c6ecb56b3c21ed94a5b7bf940d857d110a", - "reference": "f5a856c6ecb56b3c21ed94a5b7bf940d857d110a", + "url": "https://api.github.com/repos/symfony/console/zipball/b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d", + "reference": "b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d", "shasum": "" }, "require": { @@ -2693,7 +2693,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.1.0" + "source": "https://github.com/symfony/console/tree/v8.1.1" }, "funding": [ { @@ -2713,20 +2713,20 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-06-16T12:55:20+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -2764,7 +2764,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -2784,7 +2784,7 @@ "type": "tidelift" } ], - "time": "2026-04-13T15:52:40+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/polyfill-ctype", @@ -3617,16 +3617,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", "shasum": "" }, "require": { @@ -3680,7 +3680,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" }, "funding": [ { @@ -3700,7 +3700,7 @@ "type": "tidelift" } ], - "time": "2026-03-28T09:44:51+00:00" + "time": "2026-06-16T09:55:08+00:00" }, { "name": "symfony/string", @@ -3794,16 +3794,16 @@ }, { "name": "symfony/translation", - "version": "v8.1.0", + "version": "v8.1.1", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693" + "reference": "342b4218630dc2cf284cedcb2080c80b13404014" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/b2bd012ca28c4acae830ee1206a5b6e35dd99693", - "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693", + "url": "https://api.github.com/repos/symfony/translation/zipball/342b4218630dc2cf284cedcb2080c80b13404014", + "reference": "342b4218630dc2cf284cedcb2080c80b13404014", "shasum": "" }, "require": { @@ -3863,7 +3863,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v8.1.0" + "source": "https://github.com/symfony/translation/tree/v8.1.1" }, "funding": [ { @@ -3883,20 +3883,20 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-06-06T11:11:44+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", - "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", "shasum": "" }, "require": { @@ -3945,7 +3945,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1" }, "funding": [ { @@ -3965,7 +3965,7 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/uid", @@ -4047,22 +4047,22 @@ }, { "name": "symfony/var-exporter", - "version": "v8.1.0", + "version": "v8.1.1", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "2dd18582c5f6c024db9fc0ff9c76d873af726f34" + "reference": "75b74315b4e4be40e5534cf9c5cc30dd0907ed71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/2dd18582c5f6c024db9fc0ff9c76d873af726f34", - "reference": "2dd18582c5f6c024db9fc0ff9c76d873af726f34", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/75b74315b4e4be40e5534cf9c5cc30dd0907ed71", + "reference": "75b74315b4e4be40e5534cf9c5cc30dd0907ed71", "shasum": "" }, "require": { "php": ">=8.4.1", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-deepclone": "^1.37" + "symfony/polyfill-deepclone": "^1.40" }, "require-dev": { "symfony/property-access": "^7.4|^8.0", @@ -4106,7 +4106,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v8.1.0" + "source": "https://github.com/symfony/var-exporter/tree/v8.1.1" }, "funding": [ { @@ -4126,7 +4126,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-06-27T09:05:56+00:00" }, { "name": "vlucas/phpdotenv", @@ -4288,16 +4288,16 @@ }, { "name": "webifycms/ext-base", - "version": "dev-main", + "version": "v0.1.0.alpha.2", "source": { "type": "git", "url": "https://github.com/webifycms/ext-base.git", - "reference": "bfa482d3413a82403b19b7ecba9afbf485e3a371" + "reference": "4dbb69b097761f1614e876013a31910bf57bd0d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webifycms/ext-base/zipball/bfa482d3413a82403b19b7ecba9afbf485e3a371", - "reference": "bfa482d3413a82403b19b7ecba9afbf485e3a371", + "url": "https://api.github.com/repos/webifycms/ext-base/zipball/4dbb69b097761f1614e876013a31910bf57bd0d3", + "reference": "4dbb69b097761f1614e876013a31910bf57bd0d3", "shasum": "" }, "require": { @@ -4334,37 +4334,16 @@ "roave/security-advisories": "dev-latest", "webifycms/dev-tools": "^1.2" }, - "default-branch": true, "type": "webifycms-extension", "autoload": { "psr-4": { "Webify\\Base\\": "src/" } }, - "autoload-dev": { - "psr-4": { - "Webify\\Base\\Test\\": "test/", - "Webify\\Base\\Dev\\": "dev/" - } - }, - "scripts": { - "analyse": [ - "phpstan" - ], - "sniff": [ - "php-cs-fixer fix --show-progress=dots --diff --dry-run" - ], - "test": [ - "phpunit test" - ], - "code-check": [ - "@analyse", - "@sniff" - ], - "code-format": [ - "php-cs-fixer fix --show-progress=dots" - ] - }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], "authors": [ { "name": "Mohammed Shifreen", @@ -4373,10 +4352,10 @@ ], "description": "The WebifyCMS base extension serves as the foundation for the entire application, holding abstractions and reusable components. It is adjusted as needed when new features are introduced.", "support": { - "source": "https://github.com/webifycms/ext-base/tree/main", - "issues": "https://github.com/webifycms/ext-base/issues" + "issues": "https://github.com/webifycms/ext-base/issues", + "source": "https://github.com/webifycms/ext-base/tree/v0.1.0.alpha.2" }, - "time": "2026-06-12T16:38:19+00:00" + "time": "2026-06-29T07:22:21+00:00" } ], "packages-dev": [ @@ -4976,16 +4955,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.95.5", + "version": "v3.95.11", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "7f86d8763063f5d2e2e2d0e1e45bb2f15895361d" + "reference": "35f98e1293283397824d7f349ce5afb8747c3cd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7f86d8763063f5d2e2e2d0e1e45bb2f15895361d", - "reference": "7f86d8763063f5d2e2e2d0e1e45bb2f15895361d", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/35f98e1293283397824d7f349ce5afb8747c3cd5", + "reference": "35f98e1293283397824d7f349ce5afb8747c3cd5", "shasum": "" }, "require": { @@ -5019,16 +4998,16 @@ "require-dev": { "facile-it/paraunit": "^1.3.1 || ^2.11.0", "infection/infection": "^0.32.7", - "justinrainbow/json-schema": "^6.8.0", + "justinrainbow/json-schema": "^6.10.0", "keradus/cli-executor": "^2.3", "mikey179/vfsstream": "^1.6.12", "php-coveralls/php-coveralls": "^2.9.1", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8", "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.55", - "symfony/polyfill-php85": "^1.37", - "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.8", - "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.11" + "symfony/polyfill-php85": "^1.38", + "symfony/var-dumper": "^5.4.48 || ^6.4.36 || ^7.4.8 || ^8.1.0", + "symfony/yaml": "^5.4.53 || ^6.4.41 || ^7.4.13 || ^8.1.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -5069,7 +5048,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.5" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.11" }, "funding": [ { @@ -5077,7 +5056,7 @@ "type": "github" } ], - "time": "2026-06-09T14:55:16+00:00" + "time": "2026-06-25T14:17:04+00:00" }, { "name": "myclabs/deep-copy", @@ -5978,16 +5957,16 @@ }, { "name": "phpunit/phpunit", - "version": "13.2.0", + "version": "13.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3796ea973f1e7698f0d432c1c66662af9764fd9a" + "reference": "60da0ff1e10a0f72ee18a24117ec3b613a346bba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3796ea973f1e7698f0d432c1c66662af9764fd9a", - "reference": "3796ea973f1e7698f0d432c1c66662af9764fd9a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/60da0ff1e10a0f72ee18a24117ec3b613a346bba", + "reference": "60da0ff1e10a0f72ee18a24117ec3b613a346bba", "shasum": "" }, "require": { @@ -6001,7 +5980,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.4.1", - "phpunit/php-code-coverage": "^14.2", + "phpunit/php-code-coverage": "^14.2.2", "phpunit/php-file-iterator": "^7.0.0", "phpunit/php-invoker": "^7.0.0", "phpunit/php-text-template": "^6.0.0", @@ -6058,7 +6037,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/13.2.0" + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.2.1" }, "funding": [ { @@ -6066,7 +6045,7 @@ "type": "other" } ], - "time": "2026-06-05T03:13:07+00:00" + "time": "2026-06-15T13:14:22+00:00" }, { "name": "react/cache", @@ -6596,21 +6575,21 @@ }, { "name": "rector/rector", - "version": "2.4.5", + "version": "2.5.2", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c" + "reference": "49ff6339174bdbdf50b0b35ecbcff14a05ac9e24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c", - "reference": "cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/49ff6339174bdbdf50b0b35ecbcff14a05ac9e24", + "reference": "49ff6339174bdbdf50b0b35ecbcff14a05ac9e24", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.56" + "phpstan/phpstan": "^2.2.2" }, "conflict": { "rector/rector-doctrine": "*", @@ -6644,7 +6623,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.4.5" + "source": "https://github.com/rectorphp/rector/tree/2.5.2" }, "funding": [ { @@ -6652,7 +6631,7 @@ "type": "github" } ], - "time": "2026-05-26T21:03:22+00:00" + "time": "2026-06-22T11:39:33+00:00" }, { "name": "roave/security-advisories", @@ -6660,12 +6639,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "111307ab063f0ca0869698eca9d14bd7be67e005" + "reference": "36ba91e82e1b493faef2c13277d6bd2669ea9f31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/111307ab063f0ca0869698eca9d14bd7be67e005", - "reference": "111307ab063f0ca0869698eca9d14bd7be67e005", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/36ba91e82e1b493faef2c13277d6bd2669ea9f31", + "reference": "36ba91e82e1b493faef2c13277d6bd2669ea9f31", "shasum": "" }, "conflict": { @@ -6682,6 +6661,7 @@ "aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7", "aimeos/aimeos-laravel": "==2021.10", "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5", + "aimeos/pagible": "<0.10.4", "airesvsg/acf-to-rest-api": "<=3.1", "akaunting/akaunting": "<2.1.13", "akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53", @@ -6766,13 +6746,14 @@ "bytefury/crater": "<6.0.2", "cachethq/cachet": "<2.5.1", "cadmium-org/cadmium-cms": "<=0.4.9", - "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10|>=5.2.10,<5.2.12|==5.3", + "cakephp/authentication": "<3.3.6|>=4,<4.1.1", + "cakephp/cakephp": "<4.5.11|>=4.6,<4.6.4|>=5,<5.1.7|>=5.2,<5.2.13|>=5.3,<5.3.6", "cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", "cardgate/magento2": "<2.0.33", "cardgate/woocommerce": "<=3.1.15", - "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4", + "cart2quote/module-quotation": ">=4.1.6,<4.4.6|>=5,<5.4.4", "cart2quote/module-quotation-encoded": ">=4.1.6,<=4.4.5|>=5,<5.4.4", - "cartalyst/sentry": "<=2.1.6", + "cartalyst/sentry": "<2.1.7", "catfan/medoo": "<1.7.5", "causal/oidc": "<4", "cecil/cecil": "<7.47.1", @@ -6798,7 +6779,7 @@ "commerceteam/commerce": ">=0.9.6,<0.9.9", "components/jquery": ">=1.0.3,<3.5", "composer/composer": "<2.2.28|>=2.3,<2.9.8", - "concrete5/concrete5": "<9.4.8", + "concrete5/concrete5": "<9.5.1", "concrete5/core": "<8.5.8|>=9,<9.1", "contao-components/mediaelement": ">=2.14.2,<2.21.1", "contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4", @@ -6810,12 +6791,13 @@ "coreshop/core-shop": "<4.1.9|==5", "corveda/phpsandbox": "<1.3.5", "cosenary/instagram": "<=2.3", + "cotonti/cotonti": "<=1", "couleurcitron/tarteaucitron-wp": "<0.3", "cpsit/typo3-mailqueue": "<0.4.5|>=0.5,<0.5.2", "craftcms/aws-s3": ">=2.0.2,<=2.2.4", "craftcms/azure-blob": ">=2.0.0.0-beta1,<=2.1", - "craftcms/cms": "<4.17.12|>=5,<5.9.18", - "craftcms/commerce": ">=4,<4.11|>=5,<5.6", + "craftcms/cms": "<4.18|>=5,<5.10", + "craftcms/commerce": ">=4,<=4.11.1|>=5,<=5.6.4", "craftcms/composer": ">=4.0.0.0-RC1-dev,<=4.10|>=5.0.0.0-RC1-dev,<=5.5.1", "craftcms/craft": ">=3.5,<=4.16.17|>=5.0.0.0-RC1-dev,<=5.8.21", "craftcms/google-cloud": ">=2.0.0.0-beta1,<=2.2", @@ -6870,7 +6852,7 @@ "drupal/commerce_alphabank_redirect": "<1.0.3", "drupal/commerce_eurobank_redirect": "<2.1.1", "drupal/config_split": "<1.10|>=2,<2.0.2", - "drupal/core": ">=6,<6.38|>=7,<7.103|>=8,<10.5.9|>=10.6,<10.6.7|>=11,<11.2.11|>=11.3,<11.3.7", + "drupal/core": ">=6,<6.38|>=7,<7.103|>=8,<10.5.10|>=10.6,<10.6.9|>=11,<11.2.12|>=11.3,<11.3.10", "drupal/core-recommended": ">=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8", "drupal/currency": "<3.5", "drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8", @@ -6942,9 +6924,10 @@ "feehi/feehicms": "<=2.1.1", "fenom/fenom": "<=2.12.1", "filament/actions": ">=3.2,<3.2.123|>=4,<=4.11.3|>=5,<=5.6.3", - "filament/filament": ">=4,<4.3.1", - "filament/infolists": ">=3,<3.2.115", - "filament/tables": ">=3,<=3.3.50|>=4,<4.8.5|>=5,<5.3.5", + "filament/filament": ">=3,<=3.3.51|>=4,<4.11.5|>=5,<5.6.5", + "filament/forms": ">=3,<=3.3.52", + "filament/infolists": ">=3,<3.2.115|>=4,<=4.11.4|>=5,<=5.6.4", + "filament/tables": ">=3,<=3.3.50|>=4,<=4.11.4|>=5,<=5.6.4", "filegator/filegator": "<7.8", "filp/whoops": "<2.1.13", "fineuploader/php-traditional-server": "<=1.2.2", @@ -6990,10 +6973,10 @@ "georgringer/news": "<10.0.4|>=11,<11.4.4|>=12,<12.3.2|>=13,<13.0.2|>=14,<14.0.3", "geshi/geshi": "<=1.0.9.1", "getformwork/formwork": "<=2.3.3", - "getgrav/grav": "<=2.0.0.0-RC1", + "getgrav/grav": "<=2.0.0.0-RC8", "getgrav/grav-plugin-api": "<1.0.0.0-beta15", "getgrav/grav-plugin-form": "<9.1", - "getkirby/cms": "<=4.9|>=5,<=5.4", + "getkirby/cms": "<=4.9.3|>=5,<=5.4.3", "getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1", "getkirby/panel": "<2.5.14", "getkirby/starterkit": "<=3.7.0.2", @@ -7010,10 +6993,10 @@ "gregwar/rst": "<1.0.3", "grumpydictator/firefly-iii": "<=6.6.2", "gugoan/economizzer": "<=0.9.0.0-beta1", - "guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5", + "guzzlehttp/guzzle": "<7.12.1", "guzzlehttp/guzzle-services": "<1.5.4", "guzzlehttp/oauth-subscriber": "<0.8.1", - "guzzlehttp/psr7": "<2.10.2", + "guzzlehttp/psr7": "<2.12.1", "haffner/jh_captcha": "<=2.1.3|>=3,<=3.0.2", "handcraftedinthealps/goodby-csv": "<1.4.3", "harvesthq/chosen": "<1.8.7", @@ -7068,6 +7051,7 @@ "jasig/phpcas": "<1.3.3", "jbartels/wec-map": "<3.0.3", "jcbrand/converse.js": "<3.3.3", + "jleehr/canto-saas-api": "<=2", "joedolson/my-calendar": "<3.7.7", "joelbutcher/socialstream": "<5.6|>=6,<6.2", "johnbillion/query-monitor": "<3.20.4", @@ -7111,7 +7095,7 @@ "lara-zeus/artemis": ">=1,<=1.0.6", "lara-zeus/dynamic-dashboard": ">=3,<=3.0.1", "laravel/fortify": "<1.11.1", - "laravel/framework": "<12.60|>=13,<13.10", + "laravel/framework": "<12.61.1|>=13,<13.12", "laravel/laravel": ">=5.4,<5.4.22", "laravel/passport": ">=13,<13.7.1", "laravel/pulse": "<1.3.1", @@ -7202,6 +7186,7 @@ "movim/moxl": ">=0.8,<=0.10", "movingbytes/social-network": "<=1.2.1", "mpdf/mpdf": "<=7.1.7", + "mtdowling/jmespath.php": "<2.9.1", "munkireport/comment": "<4", "munkireport/managedinstalls": "<2.6", "munkireport/munki_facts": "<1.5", @@ -7271,6 +7256,7 @@ "paragonie/random_compat": "<2", "paragonie/sodium_compat": "<1.24|>=2,<2.5", "passbolt/passbolt_api": "<4.6.2", + "paymenter/paymenter": "<1.5", "paypal/adaptivepayments-sdk-php": "<=3.9.2", "paypal/invoice-sdk-php": "<=3.9", "paypal/merchant-sdk-php": "<3.12", @@ -7288,19 +7274,21 @@ "phenx/php-svg-lib": "<0.5.2", "php-censor/php-censor": "<2.0.13|>=2.1,<2.1.5", "php-mod/curl": "<2.3.2", - "phpbb/phpbb": "<3.3.11", + "php-standard-library/h2": ">=6.1,<6.1.2|>=6.2,<6.2.1", + "php-standard-library/php-standard-library": ">=6.1,<6.1.2|>=6.2,<6.2.1", + "phpbb/phpbb": "<3.3.16|==4.0.0.0-alpha1", "phpems/phpems": ">=6,<=6.1.3", "phpfastcache/phpfastcache": "<6.1.5|>=7,<7.1.2|>=8,<8.0.7", "phpmailer/phpmailer": "<6.5", "phpmussel/phpmussel": ">=1,<1.6", "phpmyadmin/phpmyadmin": "<5.2.2", - "phpmyfaq/phpmyfaq": "<4.1.3", + "phpmyfaq/phpmyfaq": "<4.1.4", "phpoffice/common": "<0.2.9", "phpoffice/math": "<=0.2", "phpoffice/phpexcel": "<=1.8.2", "phpoffice/phpspreadsheet": "<=1.30.4|>=2,<=2.1.15|>=2.2,<=2.4.4|>=3,<=3.10.4|>=4,<=5.6", "phppgadmin/phppgadmin": "<=7.13", - "phpseclib/phpseclib": "<=2.0.53|>=3,<=3.0.51", + "phpseclib/phpseclib": "<=2.0.54|>=3,<=3.0.53", "phpservermon/phpservermon": "<3.6", "phpsysinfo/phpsysinfo": "<3.4.3", "phpunit/phpunit": "<8.5.52|>=9,<9.6.33|>=10,<10.5.62|>=11,<11.5.50|>=12,<12.5.8|>=12.5.21,<12.5.22|>=13.1.5,<13.1.6", @@ -7316,7 +7304,7 @@ "pimcore/demo": "<10.3", "pimcore/ecommerce-framework-bundle": "<1.0.10", "pimcore/perspective-editor": "<1.5.1", - "pimcore/pimcore": "<=12.3.6", + "pimcore/pimcore": "<=12.3.8", "pimcore/web2print-tools-bundle": "<=5.2.1|>=6.0.0.0-RC1-dev,<=6.1", "piwik/piwik": "<1.11", "pixelfed/pixelfed": "<0.12.5", @@ -7324,6 +7312,7 @@ "pocketmine/bedrock-protocol": "<8.0.2", "pocketmine/pocketmine-mp": "<5.42.1", "pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1", + "pontedilana/php-weasyprint": "<=2.5.1", "poweradmin/poweradmin": "<4.2.4|>=4.3,<4.3.3", "pressbooks/pressbooks": "<5.18", "prestashop/autoupgrade": ">=4,<4.10.1", @@ -7340,8 +7329,8 @@ "prestashop/ps_linklist": "<3.1", "privatebin/privatebin": "<1.4|>=1.5,<1.7.4|>=1.7.7,<2.0.3", "processwire/processwire": "<=3.0.255", - "propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7", - "propel/propel1": ">=1,<=1.7.1", + "propel/propel": ">=2.0.0.0-alpha1,<2.0.0.0-alpha8", + "propel/propel1": ">=1,<1.7.2", "psy/psysh": "<=0.11.22|>=0.12,<=0.12.18", "pterodactyl/panel": "<1.12.3", "ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2", @@ -7402,10 +7391,10 @@ "silverstripe-australia/advancedreports": ">=1,<=2", "silverstripe/admin": "<1.13.19|>=2,<2.1.8", "silverstripe/assets": "<2.4.5|>=3,<3.1.3", - "silverstripe/cms": "<4.11.3", + "silverstripe/cms": "<6.2.1", "silverstripe/comments": ">=1.3,<3.1.1", - "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", - "silverstripe/framework": "<5.3.23", + "silverstripe/forum": "<0.6.2|>=0.7,<0.7.4", + "silverstripe/framework": "<6.2.2", "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.8.2|>=4,<4.3.7|>=5,<5.1.3", "silverstripe/hybridsessions": ">=1,<2.4.1|>=2.5,<2.5.1", "silverstripe/recipe-cms": ">=4.5,<4.5.3", @@ -7415,7 +7404,8 @@ "silverstripe/silverstripe-omnipay": "<2.5.2|>=3,<3.0.2|>=3.1,<3.1.4|>=3.2,<3.2.1", "silverstripe/subsites": ">=2,<2.6.1", "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1", - "silverstripe/userforms": "<3|>=5,<5.4.2", + "silverstripe/userforms": "<6.4.9|>=7,<7.0.7|>=7.1,<7.1.1", + "silverstripe/versioned": "<3.2.1", "silverstripe/versioned-admin": ">=1,<1.11.1", "simogeo/filemanager": "<=2.5", "simple-updates/phpwhois": "<=1", @@ -7434,12 +7424,13 @@ "sjbr/sr-freecap": "<2.4.6|>=2.5,<2.5.3", "sjbr/static-info-tables": "<2.3.1", "slim/psr7": "<1.4.1|>=1.5,<1.5.1|>=1.6,<1.6.1", - "slim/slim": "<2.6", + "slim/slim": "<2.6|>=4.4,<=4.15.1", "slub/slub-events": "<3.0.3", "smarty/smarty": "<4.5.3|>=5,<5.1.1", - "snipe/snipe-it": "<8.4.1", + "snipe/snipe-it": "<=8.6.1", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", + "solidinvoice/solidinvoice": "<=2.3.15", "solspace/craft-freeform": "<4.1.29|>=5,<=5.14.6", "soosyze/soosyze": "<=2", "spatie/browsershot": "<5.0.5", @@ -7448,6 +7439,7 @@ "spencer14420/sp-php-email-handler": "<1", "spipu/html2pdf": "<5.2.8", "spiral/roadrunner": "<2025.1", + "spomky-labs/otphp": "<11.4.3", "spoon/library": "<1.4.1", "spoonity/tcpdf": "<6.2.22", "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", @@ -7456,7 +7448,7 @@ "starcitizentools/short-description": ">=4,<4.0.1", "starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1", "starcitizenwiki/embedvideo": "<=4", - "statamic/cms": "<5.73.22|>=6,<6.18.1", + "statamic/cms": "<5.74|>=6,<6.20.3", "stormpath/sdk": "<9.9.99", "studio-42/elfinder": "<=2.1.67", "studiomitte/friendlycaptcha": "<0.1.4", @@ -7476,6 +7468,7 @@ "sylius/paypal-plugin": "<1.6.2|>=1.7,<1.7.2|>=2,<2.0.2", "sylius/resource-bundle": ">=1,<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", "sylius/sylius": "<1.9.12|>=1.10,<1.10.16|>=1.11,<1.11.17|>=1.12,<=1.12.22|>=1.13,<=1.13.14|>=1.14,<=1.14.17|>=2,<=2.0.15|>=2.1,<=2.1.11|>=2.2,<=2.2.2", + "symbiote/silverstripe-advancedworkflow": "<6.4.5|>=7,<7.1.3|>=7.2,<7.2.1", "symbiote/silverstripe-multivaluefield": ">=3,<3.1", "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4", "symbiote/silverstripe-seed": "<6.0.3", @@ -7521,7 +7514,9 @@ "symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8|>=6.4.24,<6.4.40", "symfony/twilio-notifier": ">=6.4,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symfony/ux-autocomplete": "<2.36|>=3,<3.1", + "symfony/ux-icons": ">=2.17,<2.36.1|>=3,<3.2", "symfony/ux-live-component": "<2.36|>=3,<3.1", + "symfony/ux-toolkit": ">=2.32,<2.36.1|>=3,<3.2", "symfony/ux-twig-component": "<2.25.1", "symfony/validator": "<5.4.43|>=6,<6.4.11|>=7,<7.1.4", "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8", @@ -7541,7 +7536,7 @@ "thelia/thelia": ">=2.1,<2.1.3", "theonedemon/phpwhois": "<=4.2.5", "thinkcmf/thinkcmf": "<6.0.8", - "thorsten/phpmyfaq": "<4.1.3", + "thorsten/phpmyfaq": "<4.1.4", "tikiwiki/tiki-manager": "<=17.1", "timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1", "tinymce/tinymce": "<7.9.3|>=8,<8.5.1", @@ -7565,7 +7560,7 @@ "twig/twig": "<3.27", "typicms/core": "<16.1.7", "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", - "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1|==14.2", + "typo3/cms-backend": "<10.4.57|>=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.31|>=14,<14.3.3", "typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", "typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", "typo3/cms-core": "<10.4.57|>=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.31|>=14,<14.3.3", @@ -7573,14 +7568,15 @@ "typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1", "typo3/cms-extensionmanager": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", "typo3/cms-felogin": ">=4.2,<4.2.3", + "typo3/cms-filelist": ">=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.31|>=14,<14.3.3", "typo3/cms-fluid": "<4.3.4|>=4.4,<4.4.1", - "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-form": "<10.4.57|>=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.31|>=14,<14.3.3", "typo3/cms-frontend": "<4.3.9|>=4.4,<4.4.5", - "typo3/cms-indexed-search": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-indexed-search": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<13.4.31|>=14,<14.3.3", "typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8|==13.4.2", "typo3/cms-lowlevel": ">=11,<=11.5.41", "typo3/cms-recordlist": ">=11,<11.5.48", - "typo3/cms-recycler": ">=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", + "typo3/cms-recycler": "<10.4.57|>=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.31|>=14,<14.3.3", "typo3/cms-redirects": ">=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", "typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30", "typo3/cms-scheduler": ">=11,<=11.5.41", @@ -7620,8 +7616,11 @@ "wapplersystems/a21glossary": "<=0.4.10", "web-auth/webauthn-framework": ">=3.3,<3.3.4|>=4.5,<4.9|>=5.2,<5.2.4|>=5.3,<5.3.1", "web-auth/webauthn-lib": ">=4.5,<4.9|>=5.2,<5.2.4", - "web-auth/webauthn-symfony-bundle": ">=5.2,<5.2.4", + "web-auth/webauthn-symfony-bundle": "<5.3.4", "web-feet/coastercms": "==5.5", + "web-token/jwt-experimental": "<=4.1.6", + "web-token/jwt-framework": "<=4.2.99", + "web-token/jwt-library": "<3.4.10|>=4,<4.0.7|>=4.1,<4.1.7", "web-tp3/wec_map": "<3.0.3", "webbuilders-group/silverstripe-kapost-bridge": "<0.4", "webcoast/deferred-image-processing": "<1.0.2", @@ -7747,7 +7746,7 @@ "type": "tidelift" } ], - "time": "2026-06-12T15:06:50+00:00" + "time": "2026-06-26T23:29:05+00:00" }, { "name": "sebastian/cli-parser", @@ -8910,16 +8909,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v8.1.0", + "version": "v8.1.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102" + "reference": "abd6c11dc468725d1627302ad10f6cd486e9e3d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f249ae3f680958b6f1f9dd76e5747cf0695b4102", - "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/abd6c11dc468725d1627302ad10f6cd486e9e3d0", + "reference": "abd6c11dc468725d1627302ad10f6cd486e9e3d0", "shasum": "" }, "require": { @@ -8972,7 +8971,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.1" }, "funding": [ { @@ -8992,20 +8991,20 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-06-09T12:28:30+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", "shasum": "" }, "require": { @@ -9052,7 +9051,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" }, "funding": [ { @@ -9072,7 +9071,7 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/filesystem", @@ -9147,16 +9146,16 @@ }, { "name": "symfony/finder", - "version": "v8.1.0", + "version": "v8.1.1", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "58d2e767a66052c1487356f953445634a8194c64" + "reference": "e2989e762c70f9490fa3a00a0ac0fae5aa97a531" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/58d2e767a66052c1487356f953445634a8194c64", - "reference": "58d2e767a66052c1487356f953445634a8194c64", + "url": "https://api.github.com/repos/symfony/finder/zipball/e2989e762c70f9490fa3a00a0ac0fae5aa97a531", + "reference": "e2989e762c70f9490fa3a00a0ac0fae5aa97a531", "shasum": "" }, "require": { @@ -9191,7 +9190,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v8.1.0" + "source": "https://github.com/symfony/finder/tree/v8.1.1" }, "funding": [ { @@ -9211,7 +9210,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-06-27T09:05:56+00:00" }, { "name": "symfony/options-resolver", @@ -9497,16 +9496,16 @@ }, { "name": "symfony/var-dumper", - "version": "v8.1.0", + "version": "v8.1.1", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e" + "reference": "40096a2515a979f3125c5c928603995b8664c62a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e", - "reference": "c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/40096a2515a979f3125c5c928603995b8664c62a", + "reference": "40096a2515a979f3125c5c928603995b8664c62a", "shasum": "" }, "require": { @@ -9560,7 +9559,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v8.1.0" + "source": "https://github.com/symfony/var-dumper/tree/v8.1.1" }, "funding": [ { @@ -9580,7 +9579,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-06-09T10:54:51+00:00" }, { "name": "theseer/tokenizer", @@ -9687,8 +9686,7 @@ "aliases": [], "minimum-stability": "dev", "stability-flags": { - "roave/security-advisories": 20, - "webifycms/ext-base": 20 + "roave/security-advisories": 20 }, "prefer-stable": true, "prefer-lowest": false, @@ -9696,5 +9694,5 @@ "php": ">=8.4" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/config.php b/config/config.php index 71aa617..2f4d4fb 100644 --- a/config/config.php +++ b/config/config.php @@ -5,7 +5,7 @@ * * @see https://webifycms.com * - * @copyright Copyright (c) 2023 WebifyCMS + * @copyright Copyright (c) 2023 - Present WebifyCMS * @license https://webifycms.com/license * @author Mohammed Shifreen */ diff --git a/config/routes.php b/config/routes.php index b5cafed..df592cc 100644 --- a/config/routes.php +++ b/config/routes.php @@ -5,7 +5,7 @@ * * @see https://webifycms.com * - * @copyright Copyright (c) 2023 WebifyCMS + * @copyright Copyright (c) 2023 - Present WebifyCMS * @license https://webifycms.com/license * @author Mohammed Shifreen */ diff --git a/dev/DevelopmentServiceProvider.php b/dev/DevelopmentServiceProvider.php index 2f586e1..468aecd 100644 --- a/dev/DevelopmentServiceProvider.php +++ b/dev/DevelopmentServiceProvider.php @@ -5,7 +5,7 @@ * * @see https://webifycms.com * - * @copyright Copyright (c) 2023 WebifyCMS + * @copyright Copyright (c) 2023 - Present WebifyCMS * @license https://webifycms.com/license * @author Mohammed Shifreen */ diff --git a/docs/architecture.md b/docs/architecture.md index 438fea6..d1753b9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -16,18 +16,6 @@ build a new extension. - [Application Layer](#application-layer) - [Infrastructure Layer](#infrastructure-layer) - [Contract Layer](#contract-layer) -- [Infrastructure](#infrastructure) - - [Environment Variables](#environment-variables) - - [Configuration](#configuration) - - [Environment](#environment) - - [Application Lifecycle](#application-lifecycle) - - [Container Building](#container-building) - - [Service Providers](#service-providers) - - [Extensions](#extensions) - - [Kernels](#kernels) - - [Controllers](#controllers) - - [Error Handling Middleware](#error-handling-middleware) - - [Complete Bootstrap Example](#complete-bootstrap-example) --- @@ -40,9 +28,9 @@ four top-level namespaces under `Webify\Base`: | Layer | Namespace | Purpose | |--------------------|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| | **Domain** | `Webify\Base\Domain` | Enterprise business rules β€” entities, value objects, domain events, domain services, and domain contracts (interfaces defined by the domain). | -| **Application** | `Webify\Base\Application` | Application-level contracts such as `ConfigInterface`. Thin glue; no infrastructure references. | -| **Infrastructure** | `Webify\Base\Infrastructure` | Framework and library implementations β€” container, kernels, HTTP middleware, database repositories, Symfony/League integrations. | -| **Contract** | `Webify\Base\Contract` | Technical utilities and interfaces (e.g. `Collection`, `KeyValueReaderInterface`) that any layer may use. Depends on no layer. | +| **Application** | `Webify\Base\Application` | Application-level contracts such as `ConfigInterface`. Thin glue; no infrastructure references. | +| **Infrastructure** | `Webify\Base\Infrastructure` | Framework and library implementations β€” container, kernels, error handling, database repositories, Symfony/League integrations. | +| **Contract** | `Webify\Base\Contract` | Technical utilities and interfaces (e.g. `Collection`, `KeyValueReaderInterface`, `ArraySearchHelper`) that any layer may use. Depends on no layer. | ### Dependency Rules @@ -180,8 +168,14 @@ src/ β”œβ”€β”€ Contract/ β”‚ β”œβ”€β”€ ArraySearchHelper.php ← Dot-notation array access trait β”‚ β”œβ”€β”€ KeyValueReaderInterface.php ← Read-only key-value contract -β”‚ └── Collection/ -β”‚ └── Collection.php ← Generic type-safe collection +β”‚ β”œβ”€β”€ Collection/ +β”‚ β”‚ └── Collection.php ← Generic type-safe collection +β”‚ β”œβ”€β”€ Exception/ +β”‚ β”‚ β”œβ”€β”€ InvalidCollectionIndexException.php +β”‚ β”‚ β”œβ”€β”€ InvalidCollectionItemTypeException.php +β”‚ β”‚ └── TranslatableExceptionInterface.php +β”‚ └── Translation/ +β”‚ └── ExceptionTranslation.php ← i18n exception DTO β”‚ β”œβ”€β”€ Domain/ β”‚ β”œβ”€β”€ Contract/ ← Domain-defined interfaces @@ -202,17 +196,31 @@ src/ β”‚ └── Infrastructure/ β”œβ”€β”€ Container/ ← DI container building + β”‚ β”œβ”€β”€ ContainerBuilderInterface.php + β”‚ └── PhpDiContainerBuilder.php β”œβ”€β”€ Contract/ ← Infrastructure-level contracts + β”‚ β”œβ”€β”€ BootstrapServiceProviderInterface.php + β”‚ β”œβ”€β”€ ErrorHandlerInterface.php + β”‚ β”œβ”€β”€ ExtensionInterface.php + β”‚ └── ServiceProviderInterface.php β”œβ”€β”€ Environment/ ← Runtime environment detection + β”‚ β”œβ”€β”€ Environment.php + β”‚ └── Type.php β”œβ”€β”€ Event/ ← Domain event publishing (League) + β”‚ └── LeagueDomainEventPublisher.php β”œβ”€β”€ Exception/ ← Infrastructure exceptions + β”‚ β”œβ”€β”€ ApplicationException.php + β”‚ └── ApplicationEnvironmentException.php β”œβ”€β”€ Kernel/ ← Entry points (HTTP, Console) - β”œβ”€β”€ Presentation/ - β”‚ └── Http/ - β”‚ β”œβ”€β”€ Controller/ ← Invokable PSR-7 controllers - β”‚ └── Middleware/ ← PSR-15 middleware + β”‚ β”œβ”€β”€ Http.php + β”‚ └── Console.php β”œβ”€β”€ Provider/ ← Service providers + β”‚ └── BaseServiceProvider.php β”œβ”€β”€ Service/ ← Implementations (config, slug, ULID) + β”‚ β”œβ”€β”€ Application.php + β”‚ β”œβ”€β”€ Config.php + β”‚ β”œβ”€β”€ SymfonyAsciiSlugify.php + β”‚ └── SymfonyUlidGenerator.php └── definitions.php ← PHP-DI wiring ``` @@ -247,9 +255,9 @@ easy to extend. |--------------------------|--------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Adapter** | `Infrastructure\Service\*` | Wraps library classes behind domain interfaces. `SymfonyAsciiSlugify` β†’ `SlugifyInterface`, `SymfonyUlidGenerator` β†’ `UlidGeneratorInterface`, `LeagueDomainEventPublisher` β†’ `DomainEventPublisherInterface`. | | **Service Provider** | `Infrastructure\Provider\*` | Two-phase registration: `getDefinitions()` adds container bindings, `bootstrap()` wires routes, middleware, and commands after the container is built. | -| **Middleware** | `Infrastructure\Presentation\Http\Middleware\*` | Each PSR-15 middleware handles one cross-cutting concern. `ErrorHandler` catches all uncaught exceptions β€” re-throwing in debug mode, logging and returning an error response in production. | +| **Error Handler** | `Infrastructure\Contract\ErrorHandlerInterface` | Defines a contract for generating error responses. The HTTP kernel catches exceptions, logs them, and delegates to the `ErrorHandlerInterface` implementation to produce an appropriate response. | | **Builder** | `Infrastructure\Container\PhpDiContainerBuilder` | Encapsulates the construction of a fully configured PHP-DI container: autowiring, compilation mode, provider loading, and extension discovery. | -| **Invokable Controller** | `Infrastructure\Presentation\Http\Controller\*` | Controllers are single-action `__invoke(ServerRequestInterface): ResponseInterface` classes. Their only dependency is the PSR-17 factory, injected via constructor (see [Controllers](#controllers)). | +| **Invokable Controller** | `Infrastructure\Presentation\Http\Controller\*` | Controllers are single-action `__invoke(ServerRequestInterface): ResponseInterface` classes. Their only dependency is the PSR-17 factory, injected via constructor. | | **Dependency Injection** | `Infrastructure\definitions.php` | Central PHP-DI definitions file that wires all PSR-7 factories, the router, logger, event dispatcher, domain event publisher, console application, and kernels. | ### Contract Layer @@ -259,480 +267,3 @@ easy to extend. | **Collection** | `Contract\Collection\Collection` | Generic abstract collection with type-safe `add`, `map`, `filter`, `reduce`, `merge`, `find`, `contains`, and iteration. The type of contained items is enforced in the concrete subclass. | | **Key-Value Reader** | `Contract\KeyValueReaderInterface` | Read-only key-value access (`has`, `get`) that keeps callers decoupled from the storage mechanism. | | **Array Search Helper** | `Contract\ArraySearchHelper` | Reusable trait for recursive dot-notation array lookups. Used by the `Config` implementation. | - ---- - -## Infrastructure - -This section covers the concrete infrastructure components you interact with -when bootstrapping and running the application. - -### Environment Variables - -Sensitive and environment-specific values are managed through `.env` files -using [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv). The `.env` file -is loaded **before** the config array, so `$_ENV` values are available when -`config/config.php` is evaluated: - -```php -// public/index.php -Dotenv::createImmutable(__DIR__ . '/..')->load(); -``` - -The supported variables are documented in `.env.example`: - -| Variable | Default | Description | -|---------------|---------------|-----------------------------------------------------| -| `APP_NAME` | `WebifyCMS` | Application name | -| `APP_ID` | `webifycms` | Application identifier | -| `APP_VERSION` | `0.0.1` | Application version | -| `APP_ENV` | `development` | Runtime environment (`production` or `development`) | -| `APP_DEBUG` | `true` | Enable/disable debug mode | - -Copy `.env.example` to `.env` and adjust as needed: - -```bash -cp .env.example .env -``` - -> **Note:** `.env` is gitignored. Never commit secrets or environment-specific -> values to the repository. - -> **Important:** Don't try to read an environment variable outside the config file, -> it should be read from the config array only. - -### Configuration - -The `ConfigInterface` (`Webify\Base\Application\Service\ConfigInterface`) -defines the application configuration contract using PHP 8.4 property hooks: - -```php -interface ConfigInterface extends KeyValueReaderInterface -{ - public string $basePath { get; } - public string $runtimePath { get; } - public string $configPath { get; } - public string $cachePath { get; } - public string $logPath { get; } -} -``` - -The concrete implementation `Webify\Base\Infrastructure\Service\Config` -accepts an associative array and computes `cachePath` and `logPath` -from `runtimePath`: - -```php -use Webify\Base\Infrastructure\Service\Config; - -$config = new Config([ - 'basePath' => '/var/www/app', - 'runtimePath' => '/var/www/app/var', - 'configPath' => '/var/www/app/config', - 'id' => 'my-app', -]); -``` - -The config also supports arbitrary key-value storage via -`get(string $key, mixed $default = null): mixed` and -`has(string $key): bool`, inherited from `KeyValueReaderInterface`. - -### Environment - -The environment is managed through `Webify\Base\Infrastructure\Environment\Environment`, -a final readonly class built via the static factory `prepare()`: - -```php -use Webify\Base\Infrastructure\Environment\Environment; -use Webify\Base\Infrastructure\Environment\Type; - -$environment = Environment::prepare($config); - -$environment->isProduction(); // true when Type::Production -$environment->isDevelopment(); // true when Type::Development -$environment->isDebugEnabled(); // controlled by config key 'debug' -``` - -The config must contain an `environment` key matching one of the `Type` enum -values (`'production'` or `'development'`). The `debug` key (boolean) enables -verbose error output. - -By default these values are sourced from the `.env` file via `$_ENV['APP_ENV']` -and `$_ENV['APP_DEBUG']`. Sensible defaults are defined in `config/config.php`, -so the application works out of the box without a `.env` file during development. - -### Application Lifecycle - -`Webify\Base\Infrastructure\Service\Application` is the central bootstrap -class. Its lifecycle follows three phases: - -1. **Instantiation** β€” validate paths and create runtime directories -2. **Bootstrap** β€” build the DI container and run bootstrap providers -3. **Run** β€” dispatch the HTTP request or console command - -```php -use Webify\Base\Infrastructure\Service\Application; -use Webify\Base\Infrastructure\Environment\Environment; -use Webify\Base\Infrastructure\Service\Config; -use Webify\Base\Infrastructure\Container\PhpDiContainerBuilder; - -$config = new Config([/* ... */]); -$environment = Environment::prepare($config); -$app = new Application($config, $environment); - -// Register providers before bootstrapping -$app->registerProvider(new MyServiceProvider()); - -// Bootstrap: builds the container, calls bootstrap() on all providers -$app->bootstrap(new PhpDiContainerBuilder()); - -// Handle HTTP request or console command -$app->run(); // HTTP -$app->runConsole(); // CLI -``` - -### Container Building - -The container is built by implementations of `ContainerBuilderInterface`. -The default is `PhpDiContainerBuilder`, which uses PHP-DI: - -- Autowiring is enabled -- `ConfigInterface` and `Environment` are pre-registered as shared services -- Provider definitions are loaded from the `providers` config array -- Extension providers are loaded from the `extensions` config array -- In production mode, compilation and proxy file generation are enabled, - writing to the cache path defined in config - -```php -use Webify\Base\Infrastructure\Container\PhpDiContainerBuilder; - -$app->bootstrap(new PhpDiContainerBuilder()); -``` - -After bootstrapping, the PSR-11 container is available via: - -```php -$container = $app->getContainer(); -$router = $container->get(Router::class); -``` - -### Service Providers - -Service providers allow extensions to register container definitions and -perform bootstrapping logic. Two interfaces are available: - -Providers can reach the application in two ways: - -1. **Direct registration** β€” passed as an instance to `$app->registerProvider()`. - The provider may have any constructor dependencies because you construct it - yourself: - - ```php - $app->registerProvider(new MyProvider($someDependency)); - ``` - -2. **Config autodiscovery** β€” listed in the `providers` config array as a - class-string. The container builder instantiates them with `new $class()`. - These providers **must have a parameterless constructor** (or no explicit - constructor at all) since the builder does not resolve their dependencies - from the container. - - ```php - $config = new Config([ - 'providers' => [ - MyProvider::class, // must work with new MyProvider() - ], - ]); - ``` - - The same applies to providers returned by an extension's `getProviders()` β€” - they are also instantiated with `new $class()` and must have no required - constructor parameters. - -If a provider needs container services, it pulls them from the `ContainerInterface` -argument passed to `bootstrap()` β€” not from its own constructor. - -**`ServiceProviderInterface`** β€” provides container definitions before -the container is built: - -```php -use Webify\Base\Infrastructure\Contract\ServiceProviderInterface; - -final class MyProvider implements ServiceProviderInterface -{ - public function getDefinitions(): array - { - return [ - 'my.service' => DI\autowire(MyService::class), - ]; - } -} -``` - -**`BootstrapServiceProviderInterface`** β€” performs bootstrapping after -the container is built (routes, middleware, commands, listeners): - -```php -use Webify\Base\Infrastructure\Contract\BootstrapServiceProviderInterface; -use Psr\Container\ContainerInterface; -use League\Route\Router; - -final class MyBootstrapProvider implements BootstrapServiceProviderInterface -{ - public function bootstrap(ContainerInterface $container): void - { - $router = $container->get(Router::class); - $router->map('GET', '/hello', new HelloController()); - } -} -``` - -A single class may implement both interfaces. The base extension's -`BaseServiceProvider` does exactly this: - -```php -final readonly class BaseServiceProvider implements - ServiceProviderInterface, - BootstrapServiceProviderInterface -{ - public function getDefinitions(): array - { - return require __DIR__ . '/../definitions.php'; - } - - public function bootstrap(ContainerInterface $container): void - { - $router = $container->get(Router::class); - $errorHandler = $container->get(ErrorHandler::class); - - $router->middleware($errorHandler); - } -} -``` - -Providers are registered before bootstrapping: - -```php -$app->registerProvider(new BaseServiceProvider()); -$app->registerProvider(new MyProvider()); -$app->bootstrap(new PhpDiContainerBuilder()); -``` - -Providers listed in the config `providers` array are automatically -instantiated and registered during container building: - -```php -$config = new Config([ - 'providers' => [ - MyProvider::class, - ], -]); -``` - -### Extensions - -Extensions represent reusable packages that bundle multiple service providers. -They implement `ExtensionInterface`: - -```php -use Webify\Base\Infrastructure\Contract\ExtensionInterface; -use Webify\Base\Infrastructure\Contract\ServiceProviderInterface; - -final class MyExtension implements ExtensionInterface -{ - public function getId(): string - { - return 'my_extension'; - } - - public function getName(): string - { - return 'My Extension'; - } - - /** @return array> */ - public function getProviders(): array - { - return [ - MyExtensionServiceProvider::class, - ]; - } - - public function getVersion(): string - { - return '1.0.0'; - } -} -``` - -Extensions are registered via the `extensions` config array: - -```php -$config = new Config([ - 'extensions' => [ - MyExtension::class, - ], -]); -``` - -During container building, each extension's providers are instantiated, -registered, and their definitions added to the container β€” just like -directly registered providers. - -### Kernels - -Two kernels handle the application's entry points. - -**HTTP Kernel** (`Webify\Base\Infrastructure\Kernel\Http`) creates a PSR-7 -server request from PHP globals, dispatches it through the League router, -and emits the response. Unmatched routes (404) receive a 302 redirect to -`/404`: - -```php -// Registered automatically as 'httpKernel' in definitions.php -$app->run(); -``` - -**Console Kernel** (`Webify\Base\Infrastructure\Kernel\Console`) wraps -the Symfony Console application and runs it: - -```php -// Registered automatically as 'consoleKernel' in definitions.php -$app->runConsole(); // returns exit code -``` - -### Controllers - -Controllers live in `Infrastructure\Presentation\Http\Controller\` and follow -a consistent pattern: - -- **Single action** β€” each controller is an invokable class (`__invoke`). - No `IndexController` with five methods; use one class per route. -- **Readonly** β€” controllers are `final readonly class` instances. -- **PSR-7 in, PSR-7 out** β€” they receive `ServerRequestInterface` and return - `ResponseInterface`. -- **Minimal dependencies** β€” the only injected dependency is a PSR-17 factory - (usually `Psr17Factory`) so they can create responses and streams. - -```php -use Nyholm\Psr7\Factory\Psr17Factory; -use Psr\Http\Message\{ResponseFactoryInterface, ResponseInterface, ServerRequestInterface, StreamFactoryInterface}; - -final readonly class Home -{ - public function __construct( - private ResponseFactoryInterface $responseFactory, - private StreamFactoryInterface $streamFactory - ) {} - - public function __invoke(ServerRequestInterface $request): ResponseInterface - { - $response = $this->responseFactory->createResponse(200); - $body = $this->streamFactory->createStream('Hello, world!'); - - return $response - ->withBody($body) - ->withHeader('Content-Type', 'text/plain') - ; - } -} -``` - -Controllers are registered as routes inside a `BootstrapServiceProvider`: - -```php -public function bootstrap(ContainerInterface $container): void -{ - $router = $container->get(Router::class); - $router->map('GET', '/', [Home::class, '__invoke']); -} -``` - -The PSR-17 factory is autowired by the container, so you never need to register -it β€” just type-hint it in the controller's constructor. - -### Error Handling Middleware - -`Webify\Base\Infrastructure\Presentation\Http\Middleware\ErrorHandler` is a -PSR-15 middleware that wraps the entire middleware pipeline: - -- **Debug mode** (`isDebugEnabled()` returns `true`): re-throws the exception - so a development tool like Whoops can render a diagnostic page. -- **Production mode**: catches the exception, logs server errors (500+) - with full context, and returns an appropriate HTTP error response with - `Content-Type: text/html; charset=utf-8`. -- HTTP exceptions (implementing `HttpExceptionInterface`) use their status - code; all others default to 500. - -```php -$router->middleware($errorHandler); // registered by BaseServiceProvider -``` - -### Complete Bootstrap Example - -```php -use Dotenv\Dotenv; -use Webify\Base\Infrastructure\Container\PhpDiContainerBuilder; -use Webify\Base\Infrastructure\Environment\Environment; -use Webify\Base\Infrastructure\Provider\BaseServiceProvider; -use Webify\Base\Infrastructure\Service\Application; -use Webify\Base\Infrastructure\Service\Config; - -// 0. Load environment variables from .env (optional β€” falls back to defaults) -Dotenv::createImmutable(__DIR__ . '/..')->safeLoad(); - -// 1. Configuration (sourced from $_ENV via config/config.php) -$config = new Config([ - 'basePath' => __DIR__ . '/..', - 'runtimePath' => __DIR__ . '/../var', - 'configPath' => __DIR__ . '/../config', - 'providers' => [ - MyProvider::class, - ], - 'extensions' => [ - MyExtension::class, - ], -]); - -// 2. Environment -$environment = Environment::prepare($config); - -// 3. Application -$app = new Application($config, $environment); - -// 4. Register core providers -$app->registerProvider(new BaseServiceProvider()); -$app->registerProvider(new RouteServiceProvider()); - -// 5. Bootstrap (builds container, runs bootstrap providers) -$app->bootstrap(new PhpDiContainerBuilder()); - -// 6. Run -$app->run(); -``` - -## Architecture Tests - -Architecture rules are enforced by **phpat** as part of the PHPStan analysis -pipeline. Run them with: - -```bash -vendor/bin/phpstan analyse -``` - -The `ArchitectureSpec` class in `test/Architecture/` defines all rules. -Tests follow a fluent pattern: - -```php -public function testRuleName(): Rule -{ - return PHPat::rule() - ->classes(Selector::inNamespace('Webify\Base\Domain')) - ->shouldNot() - ->dependOn() - ->classes(Selector::inNamespace('Webify\Base\Infrastructure')) - ->because('description of the rule'); -} -``` - -Adding a new rule is straightforward β€” create a new method returning `Rule` -in `ArchitectureSpec.php`. diff --git a/docs/guide.md b/docs/guide.md new file mode 100644 index 0000000..60f3ef2 --- /dev/null +++ b/docs/guide.md @@ -0,0 +1,629 @@ +# WebifyCMS Guide + +A practical walkthrough of bootstrapping, configuring, and extending a +WebifyCMS application. This guide covers the infrastructure layer β€” +everything from environment variables and configuration to service +providers, extensions, kernels, controllers, and error handling. + +--- + +- [Environment Variables](#environment-variables) + - [Available Variables](#available-variables) + - [Setup](#setup) +- [Configuration](#configuration) + - [The Config Interface](#the-config-interface) + - [Creating a Config Instance](#creating-a-config-instance) + - [Reading Values](#reading-values) + - [Immutability with `with()`](#immutability-with-with) +- [Environment](#environment) + - [Factory Method](#factory-method) + - [Runtime Checks](#runtime-checks) +- [Application Lifecycle](#application-lifecycle) + - [Phase 1: Instantiation](#phase-1-instantiation) + - [Phase 2: Bootstrap](#phase-2-bootstrap) + - [Phase 3: Run](#phase-3-run) +- [Container Building](#container-building) + - [Container Builder Interface](#container-builder-interface) + - [PHP-DI Builder](#php-di-builder) +- [Service Providers](#service-providers) + - [ServiceProviderInterface](#serviceproviderinterface) + - [BootstrapServiceProviderInterface](#bootstrapserviceproviderinterface) + - [Registration Methods](#registration-methods) + - [Constructor Constraints](#constructor-constraints) +- [Extensions](#extensions) + - [Extension Interface](#extension-interface) + - [Registration](#registration) +- [Kernels](#kernels) + - [HTTP Kernel](#http-kernel) + - [Console Kernel](#console-kernel) +- [Controllers](#controllers) + - [Structure](#structure) + - [Registration](#registration) +- [Error Handling](#error-handling) + - [Error Handler Interface](#error-handler-interface) + - [Flow](#flow) +- [Complete Bootstrap Example](#complete-bootstrap-example) +- [Architecture Tests](#architecture-tests) + - [Running Tests](#running-tests) + - [Writing Rules](#writing-rules) + +--- + +## Environment Variables + +Sensitive and environment-specific values are managed through `.env` files +using [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv). The `.env` file +is loaded **before** the config array, so `$_ENV` values are available when +`config/config.php` is evaluated. + +### Available Variables + +The supported variables are documented in `.env.example`: + +| Variable | Default | Description | +|---------------|---------------|-----------------------------------------------------| +| `APP_NAME` | `WebifyCMS` | Application name | +| `APP_ID` | `webifycms` | Application identifier | +| `APP_VERSION` | `0.0.1` | Application version | +| `APP_ENV` | `development` | Runtime environment (`production` or `development`) | +| `APP_DEBUG` | `true` | Enable/disable debug mode | + +### Setup + +Copy `.env.example` to `.env` and adjust as needed: + +```bash +cp .env.example .env +``` + +> **Note:** `.env` is gitignored. Never commit secrets or environment-specific +> values to the repository. + +> **Important:** Don't try to read an environment variable outside the config file, +> it should be read from the config array only. + +--- + +## Configuration + +The configuration system is built around the `ConfigInterface` and its concrete +implementation `Config`. + +### The Config Interface + +`ConfigInterface` (`Webify\Base\Application\Service\ConfigInterface`) defines +the application configuration contract using PHP 8.4 property hooks: + +```php +interface ConfigInterface extends KeyValueReaderInterface +{ + public const string CACHE_DIR = 'cache'; + public const string LOG_DIR = 'log'; + + public string $basePath { get; } + public string $runtimePath { get; } + public string $configPath { get; } + public string $cachePath { get; } + public string $logPath { get; } + public string $baseUrl { get; } + + public function with(array $config): ConfigInterface; +} +``` + +### Creating a Config Instance + +The concrete implementation `Webify\Base\Infrastructure\Service\Config` accepts +an associative array and computes `cachePath` and `logPath` from `runtimePath`: + +```php +use Webify\Base\Infrastructure\Service\Config; + +$config = new Config([ + 'basePath' => '/var/www/app', + 'runtimePath' => '/var/www/app/var', + 'configPath' => '/var/www/app/config', + 'baseUrl' => 'https://example.com', + 'id' => 'my-app', +]); +``` + +### Reading Values + +The config also supports arbitrary key-value storage via +`get(string $key, mixed $default = null): mixed` and +`has(string $key): bool`, inherited from `KeyValueReaderInterface`. + +### Immutability with `with()` + +The `with()` method returns a new instance with the given configuration +merged in, enabling immutable config overrides: + +```php +$config = $baseConfig->with(['debug' => true]); +``` + +--- + +## Environment + +The environment is managed through `Webify\Base\Infrastructure\Environment\Environment`, +a final readonly class. + +### Factory Method + +The environment is created via the static factory `prepare()`: + +```php +use Webify\Base\Infrastructure\Environment\Environment; +use Webify\Base\Infrastructure\Environment\Type; + +$environment = Environment::prepare($config); +``` + +### Runtime Checks + +The environment object provides several runtime checks: + +```php +$environment->isProduction(); // true when Type::Production +$environment->isDevelopment(); // true when Type::Development +$environment->isDebugEnabled(); // controlled by config key 'debug' +``` + +The config must contain an `environment` key matching one of the `Type` enum +values (`'production'` or `'development'`). The `debug` key (boolean) enables +verbose error output. + +By default these values are sourced from the `.env` file via `$_ENV['APP_ENV']` +and `$_ENV['APP_DEBUG']`. Sensible defaults are defined in `config/config.php`, +so the application works out of the box without a `.env` file during development. + +--- + +## Application Lifecycle + +`Webify\Base\Infrastructure\Service\Application` is the central bootstrap +class. Its lifecycle follows three phases. + +### Phase 1: Instantiation + +Validate paths and create runtime directories. + +```php +$app = new Application($config, $environment); +``` + +### Phase 2: Bootstrap + +Build the DI container and run bootstrap providers. Providers must be +registered *before* bootstrapping. + +```php +$app->registerProvider(new MyServiceProvider()); +$app->bootstrap(new PhpDiContainerBuilder()); +``` + +### Phase 3: Run + +Dispatch the HTTP request or console command. + +```php +$app->run(); // HTTP +$app->runConsole(); // CLI (returns exit code) +``` + +--- + +## Container Building + +The container is built by implementations of `ContainerBuilderInterface`. + +### Container Builder Interface + +```php +interface ContainerBuilderInterface +{ + public function build(Application $app): ContainerInterface; +} +``` + +### PHP-DI Builder + +The default is `PhpDiContainerBuilder`, which uses PHP-DI. The builder +receives the `Application` instance, giving it access to the config, +environment, and any previously registered providers: + +- Autowiring is enabled +- `ConfigInterface` and `Environment` are pre-registered as shared services +- Provider definitions are loaded from the `providers` config array +- Extension providers are loaded from the `extensions` config array +- In production mode, compilation and proxy file generation are enabled, + writing to the cache path defined in config + +```php +use Webify\Base\Infrastructure\Container\PhpDiContainerBuilder; + +$app->bootstrap(new PhpDiContainerBuilder()); +``` + +After bootstrapping, the PSR-11 container is available anywhere in your +application by type-hinting `Psr\Container\ContainerInterface`. The container +registers itself as a shared service, so it can be injected into constructors +of controllers, service providers, or any other class resolved by the DI +container: + +```php +use Psr\Container\ContainerInterface; + +final readonly class SomeService +{ + public function __construct( + private ContainerInterface $container, + ) {} + + public function doSomething(): void + { + $router = $this->container->get(Router::class); + // ... + } +} +``` + +--- + +## Service Providers + +Service providers allow extensions to register container definitions and +perform bootstrapping logic. Two interfaces are available. + +### ServiceProviderInterface + +Provides container definitions **before** the container is built: + +```php +use Webify\Base\Infrastructure\Contract\ServiceProviderInterface; + +final class MyProvider implements ServiceProviderInterface +{ + public function getDefinitions(): array + { + return [ + 'my.service' => DI\autowire(MyService::class), + ]; + } +} +``` + +### BootstrapServiceProviderInterface + +Performs bootstrapping **after** the container is built (routes, middleware, +commands, listeners): + +```php +use Webify\Base\Infrastructure\Contract\BootstrapServiceProviderInterface; +use Psr\Container\ContainerInterface; +use League\Route\Router; + +final class MyBootstrapProvider implements BootstrapServiceProviderInterface +{ + public function bootstrap(ContainerInterface $container): void + { + $router = $container->get(Router::class); + $router->map('GET', '/hello', new HelloController()); + } +} +``` + +A single class may implement both interfaces. The base extension's +`BaseServiceProvider` implements `ServiceProviderInterface` and delegates +definition loading to the central `definitions.php` file: + +```php +final readonly class BaseServiceProvider implements ServiceProviderInterface +{ + public function getDefinitions(): array + { + return require __DIR__ . '/../definitions.php'; + } +} +``` + +### Registration Methods + +Providers can reach the application in two ways: + +**Direct registration** β€” passed as an instance to `$app->registerProvider()`. +The provider may have any constructor dependencies because you construct it +yourself: + +```php +$app->registerProvider(new MyProvider($someDependency)); +``` + +**Config autodiscovery** β€” listed in the `providers` config array as a +class-string. The container builder instantiates them with `new $class()`. + +```php +$config = new Config([ + 'providers' => [ + MyProvider::class, + ], +]); +``` + +### Constructor Constraints + +Providers listed in the config `providers` array **must have a parameterless +constructor** (or no explicit constructor at all) since the builder does not +resolve their dependencies from the container. + +The same applies to providers returned by an extension's `getProviders()` β€” +they are also instantiated with `new $class()` and must have no required +constructor parameters. + +If a provider needs container services, it pulls them from the `ContainerInterface` +argument passed to `bootstrap()` β€” not from its own constructor. + +Providers are registered before bootstrapping: + +```php +$app->registerProvider(new BaseServiceProvider()); +$app->registerProvider(new MyProvider()); +$app->bootstrap(new PhpDiContainerBuilder()); +``` + +--- + +## Extensions + +Extensions represent reusable packages that bundle multiple service providers. + +### Extension Interface + +They implement `ExtensionInterface`: + +```php +use Webify\Base\Infrastructure\Contract\ExtensionInterface; +use Webify\Base\Infrastructure\Contract\ServiceProviderInterface; + +final class MyExtension implements ExtensionInterface +{ + public function getId(): string + { + return 'my_extension'; + } + + public function getName(): string + { + return 'My Extension'; + } + + /** @return array> */ + public function getProviders(): array + { + return [ + MyExtensionServiceProvider::class, + ]; + } + + public function getVersion(): string + { + return '1.0.0'; + } +} +``` + +### Registration + +Extensions are registered via the `extensions` config array: + +```php +$config = new Config([ + 'extensions' => [ + MyExtension::class, + ], +]); +``` + +During container building, each extension's providers are instantiated, +registered, and their definitions added to the container β€” just like +directly registered providers. + +--- + +## Kernels + +Two kernels handle the application's entry points. + +### HTTP Kernel + +`Webify\Base\Infrastructure\Kernel\Http` creates a PSR-7 server request from +PHP globals, dispatches it through the League router, and emits the response. +Errors are handled internally β€” logged, then passed to `ErrorHandlerInterface` +to produce an appropriate HTTP response. + +In debug mode non-404 exceptions are re-thrown so a development tool like +Whoops can render a diagnostic page: + +```php +// Registered automatically as 'httpKernel' in definitions.php +$app->run(); +``` + +### Console Kernel + +`Webify\Base\Infrastructure\Kernel\Console` wraps the Symfony Console +application and runs it: + +```php +// Registered automatically as 'consoleKernel' in definitions.php +$app->runConsole(); // returns exit code +``` + +--- + +## Controllers + +Controllers live in `Infrastructure\Presentation\Http\Controller\` and follow +a consistent pattern. + +### Structure + +- **Single action** β€” each controller is an invokable class (`__invoke`). + No `IndexController` with five methods; use one class per route. +- **Readonly** β€” controllers are `final readonly class` instances. +- **PSR-7 in, PSR-7 out** β€” they receive `ServerRequestInterface` and return + `ResponseInterface`. +- **Minimal dependencies** β€” the only injected dependency is a PSR-17 factory + (usually `Psr17Factory`) so they can create responses and streams. + +```php +use Nyholm\Psr7\Factory\Psr17Factory; +use Psr\Http\Message\{ResponseFactoryInterface, ResponseInterface, ServerRequestInterface, StreamFactoryInterface}; + +final readonly class Home +{ + public function __construct( + private ResponseFactoryInterface $responseFactory, + private StreamFactoryInterface $streamFactory + ) {} + + public function __invoke(ServerRequestInterface $request): ResponseInterface + { + $response = $this->responseFactory->createResponse(200); + $body = $this->streamFactory->createStream('Hello, world!'); + + return $response + ->withBody($body) + ->withHeader('Content-Type', 'text/plain') + ; + } +} +``` + +### Registration + +Controllers are registered as routes inside a `BootstrapServiceProvider`: + +```php +public function bootstrap(ContainerInterface $container): void +{ + $router = $container->get(Router::class); + $router->map('GET', '/', [Home::class, '__invoke']); +} +``` + +The PSR-17 factory is autowired by the container, so you never need to register +it β€” just type-hint it in the controller's constructor. + +--- + +## Error Handling + +Error handling is managed through `Webify\Base\Infrastructure\Contract\ErrorHandlerInterface`. + +### Error Handler Interface + +```php +use Psr\Http\Message\{ResponseInterface, ServerRequestInterface}; +use Throwable; + +interface ErrorHandlerInterface +{ + public function handle(ServerRequestInterface $request, Throwable $throwable): ResponseInterface; +} +``` + +### Flow + +The interface is consumed by the HTTP kernel, not as middleware. When a +`Throwable` is caught during request dispatch: + +1. The error is **logged** with full context (message, file, line, URI, method). +2. In **debug mode** (`isDebugEnabled()` returns `true`): the exception is + re-thrown so Whoops (or a similar tool) renders a diagnostic page. 404 + exceptions are always handled gracefully and never re-thrown. +3. In **production mode**: the exception is passed to the `ErrorHandlerInterface` + implementation, which returns an appropriate HTTP error response. + +Applications provide their own `ErrorHandlerInterface` implementation. The +base extension does not ship a concrete error handler β€” it only defines the +contract. + +--- + +## Complete Bootstrap Example + +The following is the actual bootstrap sequence used by the application's +entry point (`public/index.php`): + +```php +use Dotenv\Dotenv; +use Webify\Base\Infrastructure\Container\PhpDiContainerBuilder; +use Webify\Base\Infrastructure\Environment\Environment; +use Webify\Base\Infrastructure\Service\{Application, Config}; + +// 0. Load environment variables from .env (optional β€” falls back to defaults) +Dotenv::createImmutable(__DIR__ . '/..')->safeLoad(); + +// 1. Load configuration array and create the Config instance +$config = new Config(require __DIR__ . '/../config/config.php'); + +// 2. Environment +$environment = Environment::prepare($config); + +// 3. Application +$app = new Application($config, $environment); + +// 4. Register additional providers (e.g. development-only) +// Core providers (BaseServiceProvider) are auto-discovered from +// the config 'providers' array during bootstrap +$app->registerProvider(new DevelopmentServiceProvider()); + +// 5. Bootstrap (builds container, runs bootstrap providers) +$app->bootstrap(new PhpDiContainerBuilder()); + +// 6. Run +$app->run(); +``` + +Core service providers (including `BaseServiceProvider`) are loaded +automatically from the config's `providers` array, so direct registration +via `registerProvider()` is only needed for providers outside that list +(such as environment-specific ones). + +--- + +## Architecture Tests + +Architecture rules are enforced by **phpat** as part of the PHPStan analysis +pipeline. + +### Running Tests + +Run them with: + +```bash +vendor/bin/phpstan analyse +``` + +### Writing Rules + +The `ArchitectureSpec` class in `test/Architecture/` defines all rules. +Tests follow a fluent pattern: + +```php +public function testRuleName(): Rule +{ + return PHPat::rule() + ->classes(Selector::inNamespace('Webify\Base\Domain')) + ->shouldNot() + ->dependOn() + ->classes(Selector::inNamespace('Webify\Base\Infrastructure')) + ->because('description of the rule'); +} +``` + +Adding a new rule is straightforward β€” create a new method returning `Rule` +in `ArchitectureSpec.php`. diff --git a/public/assets/app.css b/public/assets/app.css new file mode 100644 index 0000000..2ac02c0 --- /dev/null +++ b/public/assets/app.css @@ -0,0 +1,93 @@ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: "DM Sans", system-ui, sans-serif; + background: #222831; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + color: #ECECEC; + font-weight: 400; +} + +.card { + background: #ECECEC; + border-radius: 1.5rem; + box-shadow: 0 20px 60px rgba(0,0,0,.3); + padding: 4rem 3.5rem; + max-width: 640px; + width: 100%; + text-align: center; + transition: transform .2s ease; +} + +.logo { + display: inline-block; + margin-bottom: 2rem; +} + +h1 { + font-size: 2.5rem; + font-weight: 800; + margin-bottom: 1.25rem; + color: #30475E; +} + +p.lead { + font-size: 1.5rem; + font-weight: 600; + color: #30475E; + margin-bottom: 1.5rem; +} + +p { + font-size: 1.25rem; + line-height: 1.7; + color: #30475E; + margin-bottom: 2rem; +} + +.footer { + font-size: .875rem; + color: #C1A57B; + border-top: 1px solid #C1A57B; + padding-top: 2rem; +} + +.code { + font-size: 6rem; + font-weight: 800; + color: #C1A57B; + line-height: 1; + margin-bottom: .5rem; +} + +.text { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1.5rem; + color: #30475E; +} + +a.button { + display: inline-block; + text-decoration: none; + background: #30475E; + color: #ECECEC; + padding: .875rem 2rem; + border-radius: .75rem; + font-weight: 600; + font-size: 1rem; + transition: background .2s ease; +} + +a.button:hover { background: #222831; } + +@media (max-width: 480px) { + .card { padding: 2.5rem 1.5rem; } + .logo { font-size: 2.5rem; } + h1 { font-size: 1.5rem; } + .code { font-size: 4rem; } +} diff --git a/public/assets/logo.png b/public/assets/logo.png new file mode 100644 index 0000000..9d13278 Binary files /dev/null and b/public/assets/logo.png differ diff --git a/public/index.php b/public/index.php index 01fe4eb..33cf535 100644 --- a/public/index.php +++ b/public/index.php @@ -5,7 +5,7 @@ * * @see https://webifycms.com * - * @copyright Copyright (c) 2023 WebifyCMS + * @copyright Copyright (c) 2023 - Present WebifyCMS * @license https://webifycms.com/license * @author Mohammed Shifreen */ diff --git a/rector.php b/rector.php index 8d97f4b..607b7c2 100644 --- a/rector.php +++ b/rector.php @@ -5,7 +5,7 @@ * * @see https://webifycms.com * - * @copyright Copyright (c) 2023 WebifyCMS + * @copyright Copyright (c) 2023 - Present WebifyCMS * @license https://webifycms.com/license * @author Mohammed Shifreen */ diff --git a/src/Infrastructure/Presentation/Console/Command/Home.php b/src/Infrastructure/Presentation/Console/Command/Home.php index 4f12d5e..cfa43e2 100644 --- a/src/Infrastructure/Presentation/Console/Command/Home.php +++ b/src/Infrastructure/Presentation/Console/Command/Home.php @@ -5,7 +5,7 @@ * * @see https://webifycms.com * - * @copyright Copyright (c) 2023 WebifyCMS + * @copyright Copyright (c) 2023 - Present WebifyCMS * @license https://webifycms.com/license * @author Mohammed Shifreen */ @@ -41,6 +41,6 @@ public function __invoke(OutputInterface $output): int */ private function message(): string { - return "Hello, world! Let's simply transform to web."; + return 'Hello, World! Thank You for Trying WebifyCMS!'; } } diff --git a/src/Infrastructure/Presentation/Http/Controller/Error.php b/src/Infrastructure/Presentation/Http/Controller/Error.php new file mode 100644 index 0000000..a7917a2 --- /dev/null +++ b/src/Infrastructure/Presentation/Http/Controller/Error.php @@ -0,0 +1,52 @@ + + */ +declare(strict_types=1); + +namespace App\Infrastructure\Presentation\Http\Controller; + +use App\Infrastructure\Service\TemplateRenderer; +use Nyholm\Psr7\Factory\Psr17Factory; +use Psr\Http\Message\{ResponseInterface, ServerRequestInterface}; + +/** + * Error controller, it renders the error page. + */ +final readonly class Error +{ + /** + * The constructor. + */ + public function __construct( + private Psr17Factory $factory, + private TemplateRenderer $templateRenderer, + ) {} + + /** + * Renders the error page. + */ + public function __invoke(ServerRequestInterface $request, int $statusCode, string $statusText): ResponseInterface + { + $body = $this->templateRenderer->render( + 'error.html.php', + [ + 'statusCode' => $statusCode, + 'statusText' => $statusText, + ], + ); + $response = $this->factory->createResponse($statusCode); + + return $response + ->withBody($this->factory->createStream($body)) + ->withHeader('Content-Type', 'text/html; charset=utf-8') + ; + } +} diff --git a/src/Infrastructure/Presentation/Http/Controller/Home.php b/src/Infrastructure/Presentation/Http/Controller/Home.php index 399e336..bf2045a 100644 --- a/src/Infrastructure/Presentation/Http/Controller/Home.php +++ b/src/Infrastructure/Presentation/Http/Controller/Home.php @@ -5,7 +5,7 @@ * * @see https://webifycms.com * - * @copyright Copyright (c) 2023 WebifyCMS + * @copyright Copyright (c) 2023 - Present WebifyCMS * @license https://webifycms.com/license * @author Mohammed Shifreen */ @@ -13,6 +13,7 @@ namespace App\Infrastructure\Presentation\Http\Controller; +use App\Infrastructure\Service\TemplateRenderer; use Psr\Http\Message\{ResponseFactoryInterface, ResponseInterface, ServerRequestInterface, StreamFactoryInterface}; /** @@ -25,7 +26,8 @@ */ public function __construct( private ResponseFactoryInterface $responseFactory, - private StreamFactoryInterface $streamFactory + private StreamFactoryInterface $streamFactory, + private TemplateRenderer $templateRenderer, ) {} /** @@ -33,12 +35,12 @@ public function __construct( */ public function __invoke(ServerRequestInterface $request): ResponseInterface { + $body = $this->templateRenderer->render('home.html.php'); $response = $this->responseFactory->createResponse(200); - $body = $this->streamFactory->createStream("Hello, world! Let's simply transform to web."); return $response - ->withBody($body) - ->withHeader('Content-Type', 'text/plain') + ->withBody($this->streamFactory->createStream($body)) + ->withHeader('Content-Type', 'text/html; charset=utf-8') ; } } diff --git a/src/Infrastructure/Provider/RegisterConsoleCommands.php b/src/Infrastructure/Provider/RegisterConsoleCommands.php index 56551d9..9dfe3a6 100644 --- a/src/Infrastructure/Provider/RegisterConsoleCommands.php +++ b/src/Infrastructure/Provider/RegisterConsoleCommands.php @@ -5,7 +5,7 @@ * * @see https://webifycms.com * - * @copyright Copyright (c) 2023 WebifyCMS + * @copyright Copyright (c) 2023 - Present WebifyCMS * @license https://webifycms.com/license * @author Mohammed Shifreen */ diff --git a/src/Infrastructure/Provider/RegisterContainerDefinitions.php b/src/Infrastructure/Provider/RegisterContainerDefinitions.php index 8447c43..c60203e 100644 --- a/src/Infrastructure/Provider/RegisterContainerDefinitions.php +++ b/src/Infrastructure/Provider/RegisterContainerDefinitions.php @@ -5,7 +5,7 @@ * * @see https://webifycms.com * - * @copyright Copyright (c) 2023 WebifyCMS + * @copyright Copyright (c) 2023 - Present WebifyCMS * @license https://webifycms.com/license * @author Mohammed Shifreen */ diff --git a/src/Infrastructure/Provider/RegisterRoutes.php b/src/Infrastructure/Provider/RegisterRoutes.php index d866a13..fc3cfe6 100644 --- a/src/Infrastructure/Provider/RegisterRoutes.php +++ b/src/Infrastructure/Provider/RegisterRoutes.php @@ -5,7 +5,7 @@ * * @see https://webifycms.com * - * @copyright Copyright (c) 2023 WebifyCMS + * @copyright Copyright (c) 2023 - Present WebifyCMS * @license https://webifycms.com/license * @author Mohammed Shifreen */ diff --git a/src/Infrastructure/Service/ErrorHandler.php b/src/Infrastructure/Service/ErrorHandler.php new file mode 100644 index 0000000..e3fccf6 --- /dev/null +++ b/src/Infrastructure/Service/ErrorHandler.php @@ -0,0 +1,62 @@ + + */ +declare(strict_types=1); + +namespace App\Infrastructure\Service; + +use App\Infrastructure\Presentation\Http\Controller\Error; +use League\Route\Http\Exception\{HttpExceptionInterface, NotFoundException}; +use Nyholm\Psr7\Factory\Psr17Factory; +use Psr\Http\Message\{ResponseInterface, ServerRequestInterface}; +use Throwable; +use Webify\Base\Infrastructure\Contract\ErrorHandlerInterface; + +/** + * Catches all uncaught exceptions within the middleware pipeline. + * + * In debug mode it re-throws so the registered development error handler + * (Whoops) can produce a rich diagnostic page. In production, it logs the + * exception and emits an appropriate HTTP error response. + */ +final readonly class ErrorHandler implements ErrorHandlerInterface +{ + public function __construct( + private Psr17Factory $factory, + private TemplateRenderer $templateRenderer, + ) {} + + /** + * {@inheritDoc} + */ + public function handle(ServerRequestInterface $request, Throwable $throwable): ResponseInterface + { + $statusCode = 500; + + if ($throwable instanceof NotFoundException) { + $statusCode = 404; + } + + if ($throwable instanceof HttpExceptionInterface) { + $statusCode = $throwable->getStatusCode(); + } + + $statusText = match ($statusCode) { + 403 => 'Forbidden', + 404 => 'Page Not Found', + 405 => 'Method Not Allowed', + 500 => 'Internal Server Error', + default => 'Error', + }; + + return new Error($this->factory, $this->templateRenderer)($request, $statusCode, $statusText); + } +} diff --git a/src/Infrastructure/Service/TemplateRenderer.php b/src/Infrastructure/Service/TemplateRenderer.php new file mode 100644 index 0000000..88f470f --- /dev/null +++ b/src/Infrastructure/Service/TemplateRenderer.php @@ -0,0 +1,66 @@ + + */ +declare(strict_types=1); + +namespace App\Infrastructure\Service; + +/** + * Template renderer is a service that renders templates. + */ +final readonly class TemplateRenderer +{ + /** + * The constructor. + * + * @param string $templatesPath the absolute path to the templates directory + * @param string $baseUrl the base URL for resolving relative paths (empty for relative) + */ + public function __construct( + private string $templatesPath, + private string $baseUrl = '', + ) {} + + /** + * Renders a template. + * + * @param string $template the template name + * @param array $data the data to be passed to the template + * + * @return string the rendered template + */ + public function render(string $template, array $data = []): string + { + $data['view'] = $this; + extract($data, EXTR_SKIP); + ob_start(); + + include $this->templatesPath . '/' . $template; + + return (string) ob_get_clean(); + } + + /** + * Resolves a relative path against the configured base URL. + * + * @param string $path the path to resolve (should start with /) + * + * @return string the fully qualified URL or relative path when no base URL is set + */ + public function url(string $path): string + { + if ('' === $this->baseUrl) { + return $path; + } + + return rtrim($this->baseUrl, '/') . '/' . ltrim($path, '/'); + } +} diff --git a/src/Infrastructure/definitions.php b/src/Infrastructure/definitions.php index cc99434..9ae4aa6 100644 --- a/src/Infrastructure/definitions.php +++ b/src/Infrastructure/definitions.php @@ -5,10 +5,35 @@ * * @see https://webifycms.com * - * @copyright Copyright (c) 2023 WebifyCMS + * @copyright Copyright (c) 2023 - Present WebifyCMS * @license https://webifycms.com/license * @author Mohammed Shifreen */ declare(strict_types=1); -return []; +use App\Infrastructure\Service\{ErrorHandler, TemplateRenderer}; +use Nyholm\Psr7\Factory\Psr17Factory; +use Psr\Container\ContainerInterface; +use Webify\Base\Application\Service\ConfigInterface; +use Webify\Base\Infrastructure\Contract\ErrorHandlerInterface; + +use function DI\factory; + +return [ + ErrorHandlerInterface::class => factory( + static function (ContainerInterface $container) { + return new ErrorHandler( + $container->get(Psr17Factory::class), + $container->get(TemplateRenderer::class), + ); + } + ), + TemplateRenderer::class => factory( + static function (ConfigInterface $config): TemplateRenderer { + return new TemplateRenderer( + $config->basePath . '/templates', + $config->baseUrl, + ); + } + ), +]; diff --git a/templates/error.html.php b/templates/error.html.php new file mode 100644 index 0000000..139be48 --- /dev/null +++ b/templates/error.html.php @@ -0,0 +1,38 @@ + + */ +declare(strict_types=1); + +use App\Infrastructure\Service\TemplateRenderer; + +/** + * @var TemplateRenderer $view + * @var int $statusCode + * @var string $statusText + */ +?> + + + + + + <?= $statusCode; ?> - <?= $statusText; ?> + + + +
+
+
+

Sorry, something went wrong. The page you are looking for might have been moved or no longer exists.

+ Back to Home +
+ + diff --git a/templates/home.html.php b/templates/home.html.php new file mode 100644 index 0000000..4f339f4 --- /dev/null +++ b/templates/home.html.php @@ -0,0 +1,48 @@ + + */ +declare(strict_types=1); + +use App\Infrastructure\Service\TemplateRenderer; + +/** + * @var TemplateRenderer $view + */ +?> + + + + + + WebifyCMS + + + + + + +
+ +

Hello, World!

+

Thank you for trying WebifyCMS!

+

We are thrilled to have you here. WebifyCMS is crafted with care to help you build beautiful, + modern web experiences β€” simply and joyfully.

+

+ Explore the Guide +

+ +
+ + diff --git a/test/Architecture/ArchitectureSpec.php b/test/Architecture/ArchitectureSpec.php index 465d759..a6a9b94 100644 --- a/test/Architecture/ArchitectureSpec.php +++ b/test/Architecture/ArchitectureSpec.php @@ -5,7 +5,7 @@ * * @see https://webifycms.com * - * @copyright Copyright (c) 2023 WebifyCMS + * @copyright Copyright (c) 2023 - Present WebifyCMS * @license https://webifycms.com/license * @author Mohammed Shifreen */