diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index c96683d..bdf13d0 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -4,7 +4,17 @@
"Bash(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Force)",
"Bash(Select-Object Mode, Name)",
"Bash(Format-Table -AutoSize)",
- "PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Force | Where-Object {$_.Name -match '^[A-Z]'} | Select-Object Mode, Name)"
+ "PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Force | Where-Object {$_.Name -match '^[A-Z]'} | Select-Object Mode, Name)",
+ "Bash(Get-ChildItem -Recurse -Depth 3)",
+ "Bash(Select-Object -Property FullName, PSIsContainer)",
+ "PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Recurse -Depth 3 | Select-Object -Property @{Name=\"Path\";Expression={$_.FullName.Substring\\(28\\)}}, PSIsContainer | Format-Table -AutoSize)",
+ "PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Depth 1 | Where-Object { $_.PSIsContainer } | ForEach-Object { $_.Name })",
+ "Bash(composer install *)",
+ "PowerShell(docker exec php-mvc-territory-app-1 cat /var/www/html/vendor/cartalyst/sentinel/src/Native/SentinelBootstrapper.php 2>&1)",
+ "PowerShell(docker exec php-mvc-territory-app-1 cat /var/www/html/vendor/cartalyst/sentinel/src/Native/Facades/Sentinel.php 2>&1)",
+ "PowerShell(docker exec php-mvc-territory-app-1 cat /var/www/html/vendor/cartalyst/sentinel/src/Native/ConfigRepository.php 2>&1)",
+ "PowerShell(docker exec php-mvc-territory-app-1 cat /var/www/html/vendor/cartalyst/sentinel/src/config/config.php 2>&1)",
+ "Bash(docker-compose exec app bash -c ' *)"
]
}
}
diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php
new file mode 100644
index 0000000..6cee947
--- /dev/null
+++ b/app/Controllers/AuthController.php
@@ -0,0 +1,71 @@
+redirect('/');
+ }
+
+ return $this->view('auth.login', ['pageTitle' => 'Login']);
+ }
+
+ public function login(): Response
+ {
+ $request = Request::capture();
+
+ if (!verify_csrf_token($request->input('_token'))) {
+ return $this->view('auth.login', [
+ 'pageTitle' => 'Login',
+ 'error' => 'Invalid request. Please try again.',
+ ]);
+ }
+
+ $credentials = [
+ 'email' => (string) $request->input('email'),
+ 'password' => (string) $request->input('password'),
+ ];
+
+ $remember = (bool) $request->input('remember');
+
+ try {
+ if (Sentinel::authenticate($credentials, $remember)) {
+ return $this->redirect('/');
+ }
+ } catch (ThrottlingException $e) {
+ return $this->view('auth.login', [
+ 'pageTitle' => 'Login',
+ 'error' => 'Too many failed attempts. Please wait ' . $e->getDelay() . ' seconds.',
+ ]);
+ }
+
+ return $this->view('auth.login', [
+ 'pageTitle' => 'Login',
+ 'error' => 'Invalid email or password.',
+ ]);
+ }
+
+ public function logout(): Response
+ {
+ $request = Request::capture();
+
+ if (!verify_csrf_token($request->input('_token'))) {
+ return $this->redirect('/');
+ }
+
+ Sentinel::logout();
+
+ return $this->redirect('/login');
+ }
+}
diff --git a/app/Views/auth/login.php b/app/Views/auth/login.php
new file mode 100644
index 0000000..3018ced
--- /dev/null
+++ b/app/Views/auth/login.php
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+ = e($error) ?>
+
+
+
+
+
+
diff --git a/app/Views/partials/header.php b/app/Views/partials/header.php
index fc9392a..698c75a 100644
--- a/app/Views/partials/header.php
+++ b/app/Views/partials/header.php
@@ -2,6 +2,8 @@
declare(strict_types=1);
+use Cartalyst\Sentinel\Native\Facades\Sentinel;
+
$navigationItems = [
['label' => 'Home', 'href' => '/'],
['label' => 'Example JSON', 'href' => '/users/123'],
@@ -9,6 +11,8 @@ $navigationItems = [
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$currentPath = is_string($currentPath) && $currentPath !== '' ? $currentPath : '/';
+
+$currentUser = Sentinel::check();
?>
@@ -37,6 +41,15 @@ $currentPath = is_string($currentPath) && $currentPath !== '' ? $currentPath : '
= e($item['label']) ?>
+
+
+
+
+ Sign in
+
diff --git a/bootstrap/sentinel.php b/bootstrap/sentinel.php
new file mode 100644
index 0000000..a08af10
--- /dev/null
+++ b/bootstrap/sentinel.php
@@ -0,0 +1,21 @@
+addConnection([
+ 'driver' => 'sqlite',
+ 'database' => realpath(__DIR__ . '/../database/app.sqlite') ?: __DIR__ . '/../database/app.sqlite',
+ 'prefix' => '',
+]);
+$capsule->setAsGlobal();
+$capsule->bootEloquent();
+
+$bootstrapper = new SentinelBootstrapper(__DIR__ . '/../config/sentinel.php');
+Sentinel::instance($bootstrapper);
diff --git a/composer.json b/composer.json
index fa53963..ea1ec0b 100644
--- a/composer.json
+++ b/composer.json
@@ -18,5 +18,11 @@
"migrate:fresh": "php scripts/migrate.php fresh",
"migrate:fresh-seed": "php scripts/migrate.php fresh --seed"
},
- "require": {}
+ "require": {
+ "php": ">=8.2",
+ "cartalyst/sentinel": "^7.0",
+ "illuminate/database": "^10.0",
+ "illuminate/events": "^10.0",
+ "symfony/http-foundation": "^6.0"
+ }
}
diff --git a/composer.lock b/composer.lock
index 75912c3..e7c7556 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,15 +4,1840 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "a9e5ff0daf78f24b652c32b38b47d81b",
- "packages": [],
+ "content-hash": "de4b60164384367176d2fa1cea77c9e1",
+ "packages": [
+ {
+ "name": "brick/math",
+ "version": "0.12.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/brick/math.git",
+ "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba",
+ "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.2",
+ "phpunit/phpunit": "^10.1",
+ "vimeo/psalm": "6.8.8"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Brick\\Math\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Arbitrary-precision arithmetic library",
+ "keywords": [
+ "Arbitrary-precision",
+ "BigInteger",
+ "BigRational",
+ "arithmetic",
+ "bigdecimal",
+ "bignum",
+ "bignumber",
+ "brick",
+ "decimal",
+ "integer",
+ "math",
+ "mathematics",
+ "rational"
+ ],
+ "support": {
+ "issues": "https://github.com/brick/math/issues",
+ "source": "https://github.com/brick/math/tree/0.12.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/BenMorel",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-28T13:11:00+00:00"
+ },
+ {
+ "name": "carbonphp/carbon-doctrine-types",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git",
+ "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/99f76ffa36cce3b70a4a6abce41dba15ca2e84cb",
+ "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/dbal": "<3.7.0 || >=4.0.0"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^3.7.0",
+ "nesbot/carbon": "^2.71.0 || ^3.0.0",
+ "phpunit/phpunit": "^10.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "KyleKatarn",
+ "email": "kylekatarnls@gmail.com"
+ }
+ ],
+ "description": "Types to use Carbon in Doctrine",
+ "keywords": [
+ "carbon",
+ "date",
+ "datetime",
+ "doctrine",
+ "time"
+ ],
+ "support": {
+ "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues",
+ "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/kylekatarnls",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/Carbon",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-11T17:09:12+00:00"
+ },
+ {
+ "name": "cartalyst/sentinel",
+ "version": "v7.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cartalyst/sentinel.git",
+ "reference": "a4221b5976f0a87ea814406f049b44df87c56d4b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cartalyst/sentinel/zipball/a4221b5976f0a87ea814406f049b44df87c56d4b",
+ "reference": "a4221b5976f0a87ea814406f049b44df87c56d4b",
+ "shasum": ""
+ },
+ "require": {
+ "cartalyst/support": "^7.0",
+ "illuminate/support": "^10.0",
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "cartalyst/php-cs-fixer-config": "^2.0",
+ "illuminate/cookie": "^10.0",
+ "illuminate/database": "^10.0",
+ "illuminate/events": "^10.0",
+ "illuminate/http": "^10.0",
+ "illuminate/session": "^10.0",
+ "mockery/mockery": "^1.0",
+ "phpunit/phpunit": "^9.0"
+ },
+ "suggest": {
+ "illuminate/database": "By default, Sentinel utilizes the powerful Illuminate database layer.",
+ "illuminate/events": "To hook into various events across Sentinel, we recommend using Illuminate's event dispatcher.",
+ "symfony/http-foundation": "Required for native implementations."
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "aliases": {
+ "Reminder": "Cartalyst\\Sentinel\\Laravel\\Facades\\Reminder",
+ "Sentinel": "Cartalyst\\Sentinel\\Laravel\\Facades\\Sentinel",
+ "Activation": "Cartalyst\\Sentinel\\Laravel\\Facades\\Activation"
+ },
+ "providers": [
+ "Cartalyst\\Sentinel\\Laravel\\SentinelServiceProvider"
+ ]
+ },
+ "component": "package",
+ "branch-alias": {
+ "dev-master": "7.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Cartalyst\\Sentinel\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Cartalyst LLC",
+ "email": "help@cartalyst.com",
+ "homepage": "https://cartalyst.com"
+ }
+ ],
+ "description": "PHP 7.3+ Fully-featured Authentication & Authorization System",
+ "keywords": [
+ "auth",
+ "cartalyst",
+ "laravel",
+ "php",
+ "security"
+ ],
+ "support": {
+ "issues": "https://github.com/cartalyst/sentinel/issues",
+ "source": "https://github.com/cartalyst/sentinel/tree/v7.0.2"
+ },
+ "time": "2023-08-10T19:54:17+00:00"
+ },
+ {
+ "name": "cartalyst/support",
+ "version": "v7.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cartalyst/support.git",
+ "reference": "21668d0808f5c4698917e69ea1adbe098f9b4728"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cartalyst/support/zipball/21668d0808f5c4698917e69ea1adbe098f9b4728",
+ "reference": "21668d0808f5c4698917e69ea1adbe098f9b4728",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "cartalyst/php-cs-fixer-config": "^2.0",
+ "illuminate/mail": "^10.0",
+ "illuminate/validation": "^10.0",
+ "mockery/mockery": "^1.0",
+ "phpunit/phpunit": "^9.0",
+ "symfony/translation": "^6.2"
+ },
+ "type": "library",
+ "extra": {
+ "component": "package",
+ "branch-alias": {
+ "dev-master": "7.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Cartalyst\\Support\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Cartalyst LLC",
+ "email": "help@cartalyst.com",
+ "homepage": "https://cartalyst.com"
+ }
+ ],
+ "description": "Support helpers.",
+ "keywords": [
+ "cartalyst",
+ "helper",
+ "laravel",
+ "support"
+ ],
+ "support": {
+ "issues": "https://github.com/cartalyst/support/issues",
+ "source": "https://github.com/cartalyst/support/tree/v7.0.0"
+ },
+ "time": "2023-02-22T21:51:13+00:00"
+ },
+ {
+ "name": "doctrine/inflector",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/inflector.git",
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b",
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^12.0 || ^13.0",
+ "phpstan/phpstan": "^1.12 || ^2.0",
+ "phpstan/phpstan-phpunit": "^1.4 || ^2.0",
+ "phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
+ "phpunit/phpunit": "^8.5 || ^12.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Inflector\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.",
+ "homepage": "https://www.doctrine-project.org/projects/inflector.html",
+ "keywords": [
+ "inflection",
+ "inflector",
+ "lowercase",
+ "manipulation",
+ "php",
+ "plural",
+ "singular",
+ "strings",
+ "uppercase",
+ "words"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/inflector/issues",
+ "source": "https://github.com/doctrine/inflector/tree/2.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-10T19:31:58+00:00"
+ },
+ {
+ "name": "illuminate/bus",
+ "version": "v10.49.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/illuminate/bus.git",
+ "reference": "053f902d546d719c3f2752f7d3805a466e317312"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/illuminate/bus/zipball/053f902d546d719c3f2752f7d3805a466e317312",
+ "reference": "053f902d546d719c3f2752f7d3805a466e317312",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/collections": "^10.0",
+ "illuminate/contracts": "^10.0",
+ "illuminate/pipeline": "^10.0",
+ "illuminate/support": "^10.0",
+ "php": "^8.1"
+ },
+ "suggest": {
+ "illuminate/queue": "Required to use closures when chaining jobs (^7.0)."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "10.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Illuminate\\Bus\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "The Illuminate Bus package.",
+ "homepage": "https://laravel.com",
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2025-03-24T11:47:24+00:00"
+ },
+ {
+ "name": "illuminate/collections",
+ "version": "v10.49.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/illuminate/collections.git",
+ "reference": "6ae9c74fa92d4e1824d1b346cd435e8eacdc3232"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/illuminate/collections/zipball/6ae9c74fa92d4e1824d1b346cd435e8eacdc3232",
+ "reference": "6ae9c74fa92d4e1824d1b346cd435e8eacdc3232",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/conditionable": "^10.0",
+ "illuminate/contracts": "^10.0",
+ "illuminate/macroable": "^10.0",
+ "php": "^8.1"
+ },
+ "suggest": {
+ "symfony/var-dumper": "Required to use the dump method (^6.2)."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "10.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "helpers.php"
+ ],
+ "psr-4": {
+ "Illuminate\\Support\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "The Illuminate Collections package.",
+ "homepage": "https://laravel.com",
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2025-09-08T19:05:53+00:00"
+ },
+ {
+ "name": "illuminate/conditionable",
+ "version": "v10.49.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/illuminate/conditionable.git",
+ "reference": "47c700320b7a419f0d188d111f3bbed978fcbd3f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/illuminate/conditionable/zipball/47c700320b7a419f0d188d111f3bbed978fcbd3f",
+ "reference": "47c700320b7a419f0d188d111f3bbed978fcbd3f",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "10.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Illuminate\\Support\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "The Illuminate Conditionable package.",
+ "homepage": "https://laravel.com",
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2025-03-24T11:47:24+00:00"
+ },
+ {
+ "name": "illuminate/container",
+ "version": "v10.49.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/illuminate/container.git",
+ "reference": "b4956de5de18524c21ef36221a8ffd7fa3b534db"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/illuminate/container/zipball/b4956de5de18524c21ef36221a8ffd7fa3b534db",
+ "reference": "b4956de5de18524c21ef36221a8ffd7fa3b534db",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/contracts": "^10.0",
+ "php": "^8.1",
+ "psr/container": "^1.1.1|^2.0.1"
+ },
+ "provide": {
+ "psr/container-implementation": "1.1|2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "10.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Illuminate\\Container\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "The Illuminate Container package.",
+ "homepage": "https://laravel.com",
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2025-03-24T11:47:24+00:00"
+ },
+ {
+ "name": "illuminate/contracts",
+ "version": "v10.49.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/illuminate/contracts.git",
+ "reference": "2393ef579e020d88e24283913c815c3e2c143323"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/illuminate/contracts/zipball/2393ef579e020d88e24283913c815c3e2c143323",
+ "reference": "2393ef579e020d88e24283913c815c3e2c143323",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1",
+ "psr/container": "^1.1.1|^2.0.1",
+ "psr/simple-cache": "^1.0|^2.0|^3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "10.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Illuminate\\Contracts\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "The Illuminate Contracts package.",
+ "homepage": "https://laravel.com",
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2025-03-24T11:47:24+00:00"
+ },
+ {
+ "name": "illuminate/database",
+ "version": "v10.49.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/illuminate/database.git",
+ "reference": "711519fa4eca9c55d4f3d6680ffca71b28317e7a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/illuminate/database/zipball/711519fa4eca9c55d4f3d6680ffca71b28317e7a",
+ "reference": "711519fa4eca9c55d4f3d6680ffca71b28317e7a",
+ "shasum": ""
+ },
+ "require": {
+ "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12",
+ "ext-pdo": "*",
+ "illuminate/collections": "^10.0",
+ "illuminate/container": "^10.0",
+ "illuminate/contracts": "^10.0",
+ "illuminate/macroable": "^10.0",
+ "illuminate/support": "^10.0",
+ "php": "^8.1"
+ },
+ "conflict": {
+ "carbonphp/carbon-doctrine-types": ">=3.0",
+ "doctrine/dbal": ">=4.0"
+ },
+ "suggest": {
+ "doctrine/dbal": "Required to rename columns and drop SQLite columns (^3.5.1).",
+ "ext-filter": "Required to use the Postgres database driver.",
+ "fakerphp/faker": "Required to use the eloquent factory builder (^1.21).",
+ "illuminate/console": "Required to use the database commands (^10.0).",
+ "illuminate/events": "Required to use the observers with Eloquent (^10.0).",
+ "illuminate/filesystem": "Required to use the migrations (^10.0).",
+ "illuminate/pagination": "Required to paginate the result set (^10.0).",
+ "symfony/finder": "Required to use Eloquent model factories (^6.2)."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "10.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Illuminate\\Database\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "The Illuminate Database package.",
+ "homepage": "https://laravel.com",
+ "keywords": [
+ "database",
+ "laravel",
+ "orm",
+ "sql"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2025-03-24T11:47:24+00:00"
+ },
+ {
+ "name": "illuminate/events",
+ "version": "v10.49.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/illuminate/events.git",
+ "reference": "4a8e4fbc95c7e46aa6152fd8c900d56e5ef538cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/illuminate/events/zipball/4a8e4fbc95c7e46aa6152fd8c900d56e5ef538cf",
+ "reference": "4a8e4fbc95c7e46aa6152fd8c900d56e5ef538cf",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/bus": "^10.0",
+ "illuminate/collections": "^10.0",
+ "illuminate/container": "^10.0",
+ "illuminate/contracts": "^10.0",
+ "illuminate/macroable": "^10.0",
+ "illuminate/support": "^10.0",
+ "php": "^8.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "10.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "functions.php"
+ ],
+ "psr-4": {
+ "Illuminate\\Events\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "The Illuminate Events package.",
+ "homepage": "https://laravel.com",
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2025-03-24T11:47:24+00:00"
+ },
+ {
+ "name": "illuminate/macroable",
+ "version": "v10.49.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/illuminate/macroable.git",
+ "reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/illuminate/macroable/zipball/dff667a46ac37b634dcf68909d9d41e94dc97c27",
+ "reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "10.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Illuminate\\Support\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "The Illuminate Macroable package.",
+ "homepage": "https://laravel.com",
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2023-06-05T12:46:42+00:00"
+ },
+ {
+ "name": "illuminate/pipeline",
+ "version": "v10.49.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/illuminate/pipeline.git",
+ "reference": "c12e4f1d8a1fbecdc1e0fa4dc9fe17b4315832e9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/illuminate/pipeline/zipball/c12e4f1d8a1fbecdc1e0fa4dc9fe17b4315832e9",
+ "reference": "c12e4f1d8a1fbecdc1e0fa4dc9fe17b4315832e9",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/contracts": "^10.0",
+ "illuminate/support": "^10.0",
+ "php": "^8.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "10.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Illuminate\\Pipeline\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "The Illuminate Pipeline package.",
+ "homepage": "https://laravel.com",
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2025-03-24T11:47:24+00:00"
+ },
+ {
+ "name": "illuminate/support",
+ "version": "v10.49.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/illuminate/support.git",
+ "reference": "28b505e671dbe119e4e32a75c78f87189d046e39"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/illuminate/support/zipball/28b505e671dbe119e4e32a75c78f87189d046e39",
+ "reference": "28b505e671dbe119e4e32a75c78f87189d046e39",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/inflector": "^2.0",
+ "ext-ctype": "*",
+ "ext-filter": "*",
+ "ext-mbstring": "*",
+ "illuminate/collections": "^10.0",
+ "illuminate/conditionable": "^10.0",
+ "illuminate/contracts": "^10.0",
+ "illuminate/macroable": "^10.0",
+ "nesbot/carbon": "^2.67",
+ "php": "^8.1",
+ "voku/portable-ascii": "^2.0"
+ },
+ "conflict": {
+ "tightenco/collect": "<5.5.33"
+ },
+ "suggest": {
+ "illuminate/filesystem": "Required to use the composer class (^10.0).",
+ "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.6).",
+ "ramsey/uuid": "Required to use Str::uuid() (^4.7).",
+ "symfony/process": "Required to use the composer class (^6.2).",
+ "symfony/uid": "Required to use Str::ulid() (^6.2).",
+ "symfony/var-dumper": "Required to use the dd function (^6.2).",
+ "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.4.1)."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "10.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "helpers.php"
+ ],
+ "psr-4": {
+ "Illuminate\\Support\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "The Illuminate Support package.",
+ "homepage": "https://laravel.com",
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2025-09-08T19:05:53+00:00"
+ },
+ {
+ "name": "nesbot/carbon",
+ "version": "2.73.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/CarbonPHP/carbon.git",
+ "reference": "9228ce90e1035ff2f0db84b40ec2e023ed802075"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/9228ce90e1035ff2f0db84b40ec2e023ed802075",
+ "reference": "9228ce90e1035ff2f0db84b40ec2e023ed802075",
+ "shasum": ""
+ },
+ "require": {
+ "carbonphp/carbon-doctrine-types": "*",
+ "ext-json": "*",
+ "php": "^7.1.8 || ^8.0",
+ "psr/clock": "^1.0",
+ "symfony/polyfill-mbstring": "^1.0",
+ "symfony/polyfill-php80": "^1.16",
+ "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0"
+ },
+ "provide": {
+ "psr/clock-implementation": "1.0"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^2.0 || ^3.1.4 || ^4.0",
+ "doctrine/orm": "^2.7 || ^3.0",
+ "friendsofphp/php-cs-fixer": "^3.0",
+ "kylekatarnls/multi-tester": "^2.0",
+ "ondrejmirtes/better-reflection": "<6",
+ "phpmd/phpmd": "^2.9",
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^0.12.99 || ^1.7.14",
+ "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20",
+ "squizlabs/php_codesniffer": "^3.4"
+ },
+ "bin": [
+ "bin/carbon"
+ ],
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Carbon\\Laravel\\ServiceProvider"
+ ]
+ },
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-2.x": "2.x-dev",
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Carbon\\": "src/Carbon/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Brian Nesbitt",
+ "email": "brian@nesbot.com",
+ "homepage": "https://markido.com"
+ },
+ {
+ "name": "kylekatarnls",
+ "homepage": "https://github.com/kylekatarnls"
+ }
+ ],
+ "description": "An API extension for DateTime that supports 281 different languages.",
+ "homepage": "https://carbon.nesbot.com",
+ "keywords": [
+ "date",
+ "datetime",
+ "time"
+ ],
+ "support": {
+ "docs": "https://carbon.nesbot.com/docs",
+ "issues": "https://github.com/briannesbitt/Carbon/issues",
+ "source": "https://github.com/briannesbitt/Carbon"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/kylekatarnls",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/Carbon#sponsor",
+ "type": "opencollective"
+ },
+ {
+ "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-01-08T20:10:23+00:00"
+ },
+ {
+ "name": "psr/clock",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/clock.git",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Clock\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for reading the clock.",
+ "homepage": "https://github.com/php-fig/clock",
+ "keywords": [
+ "clock",
+ "now",
+ "psr",
+ "psr-20",
+ "time"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/clock/issues",
+ "source": "https://github.com/php-fig/clock/tree/1.0.0"
+ },
+ "time": "2022-11-25T14:36:26+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ },
+ {
+ "name": "psr/simple-cache",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/simple-cache.git",
+ "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865",
+ "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\SimpleCache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for simple caching",
+ "keywords": [
+ "cache",
+ "caching",
+ "psr",
+ "psr-16",
+ "simple-cache"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
+ },
+ "time": "2021-10-29T13:26:27+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b",
+ "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "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"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-13T15:52:40+00:00"
+ },
+ {
+ "name": "symfony/http-foundation",
+ "version": "v6.4.35",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-foundation.git",
+ "reference": "cffffd0a2c037117b742b4f8b379a22a2a33f6d2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/cffffd0a2c037117b742b4f8b379a22a2a33f6d2",
+ "reference": "cffffd0a2c037117b742b4f8b379a22a2a33f6d2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "~1.1",
+ "symfony/polyfill-php83": "^1.27"
+ },
+ "conflict": {
+ "symfony/cache": "<6.4.12|>=7.0,<7.1.5"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^2.13.1|^3|^4",
+ "predis/predis": "^1.1|^2.0",
+ "symfony/cache": "^6.4.12|^7.1.5",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0",
+ "symfony/mime": "^5.4|^6.0|^7.0",
+ "symfony/rate-limiter": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpFoundation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Defines an object-oriented layer for the HTTP specification",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/http-foundation/tree/v6.4.35"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-03-06T11:15:58+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.37.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
+ "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
+ "shasum": ""
+ },
+ "require": {
+ "ext-iconv": "*",
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-10T17:25:58+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php80",
+ "version": "v1.37.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
+ "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-10T16:19:22+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php83",
+ "version": "v1.37.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php83.git",
+ "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149",
+ "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php83\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-10T17:25:58+00:00"
+ },
+ {
+ "name": "symfony/translation",
+ "version": "v6.4.38",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation.git",
+ "reference": "afaa31b0c12d9a659eed1ea97f268a614cc1299c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/afaa31b0c12d9a659eed1ea97f268a614cc1299c",
+ "reference": "afaa31b0c12d9a659eed1ea97f268a614cc1299c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/translation-contracts": "^2.5|^3.0"
+ },
+ "conflict": {
+ "symfony/config": "<5.4",
+ "symfony/console": "<5.4",
+ "symfony/dependency-injection": "<5.4",
+ "symfony/http-client-contracts": "<2.5",
+ "symfony/http-kernel": "<5.4",
+ "symfony/service-contracts": "<2.5",
+ "symfony/twig-bundle": "<5.4",
+ "symfony/yaml": "<5.4"
+ },
+ "provide": {
+ "symfony/translation-implementation": "2.3|3.0"
+ },
+ "require-dev": {
+ "nikic/php-parser": "^4.18|^5.0",
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/http-client-contracts": "^2.5|^3.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/intl": "^5.4|^6.0|^7.0",
+ "symfony/polyfill-intl-icu": "^1.21",
+ "symfony/routing": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\Translation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools to internationalize your application",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/translation/tree/v6.4.38"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-06T08:55:54+00:00"
+ },
+ {
+ "name": "symfony/translation-contracts",
+ "version": "v3.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation-contracts.git",
+ "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d",
+ "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Translation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to translation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-05T13:30:16+00:00"
+ },
+ {
+ "name": "voku/portable-ascii",
+ "version": "2.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/voku/portable-ascii.git",
+ "reference": "8e1051fe39379367aecf014f41744ce7539a856f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f",
+ "reference": "8e1051fe39379367aecf014f41744ce7539a856f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5"
+ },
+ "suggest": {
+ "ext-intl": "Use Intl for transliterator_transliterate() support"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "voku\\": "src/voku/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Lars Moelleken",
+ "homepage": "https://www.moelleken.org/"
+ }
+ ],
+ "description": "Portable ASCII library - performance optimized (ascii) string functions for php.",
+ "homepage": "https://github.com/voku/portable-ascii",
+ "keywords": [
+ "ascii",
+ "clean",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/voku/portable-ascii/issues",
+ "source": "https://github.com/voku/portable-ascii/tree/2.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.me/moelleken",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/voku",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/portable-ascii",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://www.patreon.com/voku",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-26T05:33:54+00:00"
+ }
+ ],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
- "platform": {},
+ "platform": {
+ "php": ">=8.2"
+ },
"platform-dev": {},
"plugin-api-version": "2.9.0"
}
diff --git a/config/sentinel.php b/config/sentinel.php
new file mode 100644
index 0000000..fd387f9
--- /dev/null
+++ b/config/sentinel.php
@@ -0,0 +1,66 @@
+ 'cartalyst_sentinel',
+
+ 'cookie' => 'cartalyst_sentinel',
+
+ 'users' => [
+ 'model' => 'Cartalyst\Sentinel\Users\EloquentUser',
+ ],
+
+ 'roles' => [
+ 'model' => 'Cartalyst\Sentinel\Roles\EloquentRole',
+ ],
+
+ 'permissions' => [
+ 'class' => 'Cartalyst\Sentinel\Permissions\StandardPermissions',
+ ],
+
+ 'persistences' => [
+ 'model' => 'Cartalyst\Sentinel\Persistences\EloquentPersistence',
+ 'single' => false,
+ ],
+
+ // Only throttle is enabled; activation can be re-added when a registration flow exists.
+ 'checkpoints' => [
+ 'throttle',
+ ],
+
+ 'activations' => [
+ 'model' => 'Cartalyst\Sentinel\Activations\EloquentActivation',
+ 'expires' => 259200,
+ 'lottery' => [2, 100],
+ ],
+
+ 'reminders' => [
+ 'model' => 'Cartalyst\Sentinel\Reminders\EloquentReminder',
+ 'expires' => 14400,
+ 'lottery' => [2, 100],
+ ],
+
+ 'throttling' => [
+ 'model' => 'Cartalyst\Sentinel\Throttling\EloquentThrottle',
+ 'global' => [
+ 'interval' => 900,
+ 'thresholds' => [
+ 10 => 1,
+ 20 => 2,
+ 30 => 4,
+ 40 => 8,
+ 50 => 16,
+ 60 => 32,
+ ],
+ ],
+ 'ip' => [
+ 'interval' => 900,
+ 'thresholds' => 5,
+ ],
+ 'user' => [
+ 'interval' => 900,
+ 'thresholds' => 5,
+ ],
+ ],
+];
diff --git a/core/helpers.php b/core/helpers.php
index fffc7cb..b00f086 100644
--- a/core/helpers.php
+++ b/core/helpers.php
@@ -94,9 +94,11 @@ function prepareSqliteDatabase(string $dsn): void
}
}
-function e(?string $value): string
-{
- return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+if (!function_exists('e')) {
+ function e(?string $value): string
+ {
+ return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+ }
}
function asset(string $path): string
diff --git a/database/migrations/20260517_000001_create_sentinel_tables.php b/database/migrations/20260517_000001_create_sentinel_tables.php
new file mode 100644
index 0000000..4ba789c
--- /dev/null
+++ b/database/migrations/20260517_000001_create_sentinel_tables.php
@@ -0,0 +1,99 @@
+execute('
+ CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ email VARCHAR(255) NOT NULL UNIQUE,
+ password VARCHAR(255) NOT NULL,
+ permissions TEXT,
+ last_login DATETIME,
+ first_name VARCHAR(255),
+ last_name VARCHAR(255),
+ created_at DATETIME,
+ updated_at DATETIME
+ )
+ ');
+
+ $database->execute('
+ CREATE TABLE IF NOT EXISTS roles (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ slug VARCHAR(255) NOT NULL UNIQUE,
+ name VARCHAR(255) NOT NULL,
+ permissions TEXT,
+ created_at DATETIME,
+ updated_at DATETIME
+ )
+ ');
+
+ $database->execute('
+ CREATE TABLE IF NOT EXISTS role_users (
+ user_id INTEGER NOT NULL,
+ role_id INTEGER NOT NULL,
+ created_at DATETIME,
+ updated_at DATETIME,
+ PRIMARY KEY (user_id, role_id)
+ )
+ ');
+
+ $database->execute('
+ CREATE TABLE IF NOT EXISTS persistences (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ code VARCHAR(255) NOT NULL UNIQUE,
+ created_at DATETIME,
+ updated_at DATETIME
+ )
+ ');
+
+ $database->execute('
+ CREATE TABLE IF NOT EXISTS activations (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ code VARCHAR(255) NOT NULL,
+ completed INTEGER NOT NULL DEFAULT 0,
+ completed_at DATETIME,
+ created_at DATETIME,
+ updated_at DATETIME
+ )
+ ');
+
+ $database->execute('
+ CREATE TABLE IF NOT EXISTS reminders (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ code VARCHAR(255) NOT NULL,
+ completed INTEGER NOT NULL DEFAULT 0,
+ completed_at DATETIME,
+ created_at DATETIME,
+ updated_at DATETIME
+ )
+ ');
+
+ $database->execute('
+ CREATE TABLE IF NOT EXISTS throttle (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER,
+ type VARCHAR(255) NOT NULL,
+ ip VARCHAR(255),
+ created_at DATETIME,
+ updated_at DATETIME
+ )
+ ');
+ }
+
+ public function down(Database $database): void
+ {
+ foreach (['throttle', 'reminders', 'activations', 'persistences', 'role_users', 'roles', 'users'] as $table) {
+ $database->execute("DROP TABLE IF EXISTS {$table}");
+ }
+ }
+};
diff --git a/public/css/site.css b/public/css/site.css
index c99ebd9..0809d55 100644
--- a/public/css/site.css
+++ b/public/css/site.css
@@ -466,3 +466,97 @@ code {
font-size: 2.5rem;
}
}
+
+/* ── Auth / Login ─────────────────────────────────────── */
+
+.auth-wrap {
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ padding: 2rem 0;
+}
+
+.auth-card {
+ width: 100%;
+ max-width: 460px;
+ background: var(--surface);
+ border: 1px solid var(--surface-border);
+ box-shadow: var(--shadow-card);
+ border-radius: 2rem;
+ padding: 2.5rem 2.5rem 2rem;
+ display: grid;
+ gap: 1.5rem;
+}
+
+.auth-card-header {
+ display: grid;
+ gap: 0.4rem;
+}
+
+.auth-card-title {
+ margin: 0;
+ font-size: 2rem;
+ letter-spacing: -0.04em;
+ line-height: 1.1;
+}
+
+.auth-card-subtitle {
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 0.97rem;
+ line-height: 1.6;
+}
+
+.auth-form {
+ display: grid;
+ gap: 1.1rem;
+}
+
+.auth-remember {
+ display: flex;
+ align-items: center;
+ gap: 0.55rem;
+ font-size: 0.93rem;
+ color: var(--text-secondary);
+ cursor: pointer;
+}
+
+.auth-remember input[type="checkbox"] {
+ width: 1rem;
+ height: 1rem;
+ accent-color: var(--accent);
+ cursor: pointer;
+}
+
+.auth-submit {
+ width: 100%;
+ justify-content: center;
+ padding: 0.95rem;
+ font-size: 1rem;
+ margin-top: 0.25rem;
+}
+
+.auth-nav-form {
+ display: inline;
+ margin: 0;
+ padding: 0;
+}
+
+.auth-nav-btn {
+ background: none;
+ border: none;
+ padding: 0.7rem 1rem;
+ font: inherit;
+ font-weight: 600;
+ color: var(--text-secondary);
+ border-radius: 999px;
+ cursor: pointer;
+ transition: background-color 160ms ease, color 160ms ease, transform 160ms ease;
+}
+
+.auth-nav-btn:hover,
+.auth-nav-btn:focus-visible {
+ color: var(--accent-strong);
+ background: rgba(29, 122, 109, 0.12);
+ transform: translateY(-1px);
+}
diff --git a/public/index.php b/public/index.php
index 9d26c73..eceeea1 100644
--- a/public/index.php
+++ b/public/index.php
@@ -11,6 +11,8 @@ use Core\Router;
ensureSessionStarted();
+require_once __DIR__ . '/../bootstrap/sentinel.php';
+
$app = new App();
$router = new Router();
diff --git a/routes/web.php b/routes/web.php
index 9a631e8..e11f652 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -3,6 +3,11 @@
declare(strict_types=1);
use App\Controllers\HomeController;
+use App\Controllers\AuthController;
$router->get('/', [HomeController::class, 'index']);
$router->get('/users/{id}', [HomeController::class, 'user']);
+
+$router->get('/login', [AuthController::class, 'showLogin']);
+$router->post('/login', [AuthController::class, 'login']);
+$router->post('/logout', [AuthController::class, 'logout']);
diff --git a/scripts/create_user.php b/scripts/create_user.php
new file mode 100644
index 0000000..a9c546d
--- /dev/null
+++ b/scripts/create_user.php
@@ -0,0 +1,19 @@
+ 'admin@example.com',
+ 'password' => 'secret123',
+ 'first_name' => 'Admin',
+ 'last_name' => 'User',
+]);
+
+echo $user ? 'User created: ' . $user->email . PHP_EOL : 'Failed' . PHP_EOL;