diff --git a/.coveralls.yml b/.coveralls.yml
new file mode 100644
index 0000000..a8172fe
--- /dev/null
+++ b/.coveralls.yml
@@ -0,0 +1 @@
+json_path: coveralls-upload.json
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..9e4c2ee
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
+version: 2
+updates:
+- package-ecosystem: composer
+ directory: "/"
+ schedule:
+ interval: monthly
+ open-pull-requests-limit: 10
+ versioning-strategy: increase
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..2934da2
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,49 @@
+name: Tests
+
+on: [push, pull_request]
+
+jobs:
+ tests:
+ name: Tests PHP ${{ matrix.php }}
+ runs-on: ubuntu-latest
+ continue-on-error: ${{ matrix.experimental }}
+ strategy:
+ fail-fast: false
+ matrix:
+ php: [7.4, 8.0, 8.1]
+ experimental: [false]
+ include:
+ - php: 8.1
+ analysis: true
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Set up PHP ${{ matrix.php }}
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ coverage: xdebug
+
+ - name: Install dependencies with Composer
+ uses: ramsey/composer-install@v2
+
+ - name: Coding standards
+ if: matrix.analysis
+ run: vendor/bin/phpcs
+
+ - name: Static analysis
+ if: matrix.analysis
+ run: vendor/bin/phpstan
+
+ - name: Tests
+ run: vendor/bin/phpunit --coverage-clover clover.xml
+
+ - name: Upload coverage results to Coveralls
+ if: matrix.analysis
+ env:
+ COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ composer require php-coveralls/php-coveralls -n -W
+ vendor/bin/php-coveralls --coverage_clover=clover.xml -v
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2ebad0b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.idea/
+.vscode/
+/coverage/
+/vendor/
+/logs/*
+!/logs/README.md
+.phpunit.result.cache
+.DS_Store
\ No newline at end of file
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..c7552bf
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,19 @@
+Options All -Indexes
+
+
+order allow,deny
+deny from all
+
+
+
+ # Redirect to the public folder
+ RewriteEngine On
+ # RewriteBase /
+ RewriteRule ^$ public/ [L]
+ RewriteRule (.*) public/$1 [L]
+
+ # Redirect to HTTPS
+ # RewriteEngine On
+ # RewriteCond %{HTTPS} off
+ # RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..c7878cb
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,14 @@
+# How to Contribute
+
+## Pull Requests
+
+1. Fork the Slim Skeleton repository
+2. Create a new branch for each feature or improvement
+3. Send a pull request from each feature branch to the **4.x** branch
+
+It is very important to separate new features or improvements into separate feature branches, and to send a
+pull request for each branch. This allows us to review and pull in new features or improvements individually.
+
+## Style Guide
+
+All pull requests must adhere to the [PSR-12 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md).
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f769efc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,66 @@
+# MTEX Mock API
+
+Simple REST API providing sample JSON data for testing and prototyping.
+
+## Installation
+
+```bash
+composer install
+```
+
+## Local Development
+
+```bash
+php -S localhost:8000 -t public
+```
+
+Visit `http://localhost:8000` to see the API documentation.
+
+## Endpoints
+
+### Documentation
+
+- `GET /` - API documentation
+
+### Users
+
+- `GET /users` - List all users
+- `GET /users/{id}` - Get user by ID
+
+### Posts
+
+- `GET /posts` - List all posts
+- `GET /posts/{id}` - Get post by ID
+
+### Products
+
+- `GET /products` - List all products
+- `GET /products/{id}` - Get product by ID
+
+## Example Requests
+
+```bash
+curl https://mock.mtex.dev/users
+curl https://mock.mtex.dev/users/1
+curl https://mock.mtex.dev/posts
+curl https://mock.mtex.dev/products/2
+```
+
+## Features
+
+- [x] RESTful API design
+- [x] JSON responses
+- [x] CORS enabled
+- [x] Error handling
+- [x] Self-documenting
+- [x] No database required
+
+## Tech Stack
+
+- PHP 8.1+
+- Slim Framework 4
+- PSR-7 HTTP Messages
+
+## License
+
+MIT
\ No newline at end of file
diff --git a/app/dependencies.php b/app/dependencies.php
new file mode 100644
index 0000000..fd3f066
--- /dev/null
+++ b/app/dependencies.php
@@ -0,0 +1,30 @@
+addDefinitions([
+ LoggerInterface::class => function (ContainerInterface $c) {
+ $settings = $c->get(SettingsInterface::class);
+
+ $loggerSettings = $settings->get('logger');
+ $logger = new Logger($loggerSettings['name']);
+
+ $processor = new UidProcessor();
+ $logger->pushProcessor($processor);
+
+ $handler = new StreamHandler($loggerSettings['path'], $loggerSettings['level']);
+ $logger->pushHandler($handler);
+
+ return $logger;
+ },
+ ]);
+};
diff --git a/app/middleware.php b/app/middleware.php
new file mode 100644
index 0000000..a3f6cf9
--- /dev/null
+++ b/app/middleware.php
@@ -0,0 +1,10 @@
+add(SessionMiddleware::class);
+};
diff --git a/app/repositories.php b/app/repositories.php
new file mode 100644
index 0000000..15f20f3
--- /dev/null
+++ b/app/repositories.php
@@ -0,0 +1,14 @@
+addDefinitions([
+ UserRepository::class => \DI\autowire(InMemoryUserRepository::class),
+ ]);
+};
diff --git a/app/routes.php b/app/routes.php
new file mode 100644
index 0000000..17e4b82
--- /dev/null
+++ b/app/routes.php
@@ -0,0 +1,27 @@
+options('/{routes:.*}', function (Request $request, Response $response) {
+ // CORS Pre-Flight OPTIONS Request Handler
+ return $response;
+ });
+
+ $app->get('/', function (Request $request, Response $response) {
+ $response->getBody()->write('Hello world!');
+ return $response;
+ });
+
+ $app->group('/users', function (Group $group) {
+ $group->get('', ListUsersAction::class);
+ $group->get('/{id}', ViewUserAction::class);
+ });
+};
diff --git a/app/settings.php b/app/settings.php
new file mode 100644
index 0000000..79392bd
--- /dev/null
+++ b/app/settings.php
@@ -0,0 +1,27 @@
+addDefinitions([
+ SettingsInterface::class => function () {
+ return new Settings([
+ 'displayErrorDetails' => true, // Should be set to false in production
+ 'logError' => false,
+ 'logErrorDetails' => false,
+ 'logger' => [
+ 'name' => 'slim-app',
+ 'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log',
+ 'level' => Logger::DEBUG,
+ ],
+ ]);
+ }
+ ]);
+};
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..d32875e
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,15 @@
+{
+ "name": "mtex/mock-api",
+ "description": "Simple REST API providing sample JSON data",
+ "type": "project",
+ "require": {
+ "php": "^8.1",
+ "slim/slim": "^4.12",
+ "slim/psr7": "^1.6"
+ },
+ "autoload": {
+ "psr-4": {
+ "App\\": "src/"
+ }
+ }
+}
\ No newline at end of file
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..b62b333
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,690 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "e6111b07c655e23e9706a7e73e61a3b4",
+ "packages": [
+ {
+ "name": "fig/http-message-util",
+ "version": "1.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message-util.git",
+ "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765",
+ "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3 || ^7.0 || ^8.0"
+ },
+ "suggest": {
+ "psr/http-message": "The package containing the PSR-7 interfaces"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Fig\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Utility classes and constants for use with PSR-7 (psr/http-message)",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/http-message-util/issues",
+ "source": "https://github.com/php-fig/http-message-util/tree/1.1.5"
+ },
+ "time": "2020-11-24T22:02:12+00:00"
+ },
+ {
+ "name": "nikic/fast-route",
+ "version": "v1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/FastRoute.git",
+ "reference": "181d480e08d9476e61381e04a71b34dc0432e812"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812",
+ "reference": "181d480e08d9476e61381e04a71b34dc0432e812",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35|~5.7"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "FastRoute\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov",
+ "email": "nikic@php.net"
+ }
+ ],
+ "description": "Fast request router for PHP",
+ "keywords": [
+ "router",
+ "routing"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/FastRoute/issues",
+ "source": "https://github.com/nikic/FastRoute/tree/master"
+ },
+ "time": "2018-02-13T20:26:39+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/http-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory"
+ },
+ "time": "2024-04-15T12:06:14+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "psr/http-server-handler",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-server-handler.git",
+ "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4",
+ "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP server-side request handler",
+ "keywords": [
+ "handler",
+ "http",
+ "http-interop",
+ "psr",
+ "psr-15",
+ "psr-7",
+ "request",
+ "response",
+ "server"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2"
+ },
+ "time": "2023-04-10T20:06:20+00:00"
+ },
+ {
+ "name": "psr/http-server-middleware",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-server-middleware.git",
+ "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
+ "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0",
+ "psr/http-message": "^1.0 || ^2.0",
+ "psr/http-server-handler": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP server-side middleware",
+ "keywords": [
+ "http",
+ "http-interop",
+ "middleware",
+ "psr",
+ "psr-15",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/http-server-middleware/issues",
+ "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2"
+ },
+ "time": "2023-04-11T06:14:47+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/3.0.2"
+ },
+ "time": "2024-09-11T13:17:53+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "slim/psr7",
+ "version": "1.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/slimphp/Slim-Psr7.git",
+ "reference": "76e7e3b1cdfd583e9035c4c966c08e01e45ce959"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/slimphp/Slim-Psr7/zipball/76e7e3b1cdfd583e9035c4c966c08e01e45ce959",
+ "reference": "76e7e3b1cdfd583e9035c4c966c08e01e45ce959",
+ "shasum": ""
+ },
+ "require": {
+ "fig/http-message-util": "^1.1.5",
+ "php": "^8.0",
+ "psr/http-factory": "^1.1",
+ "psr/http-message": "^1.0 || ^2.0",
+ "ralouphie/getallheaders": "^3.0"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "^1.0",
+ "psr/http-message-implementation": "^1.0 || ^2.0"
+ },
+ "require-dev": {
+ "adriansuter/php-autoload-override": "^1.5|| ^2.0",
+ "ext-json": "*",
+ "http-interop/http-factory-tests": "^1.0 || ^2.0",
+ "php-http/psr7-integration-tests": "^1.5",
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^9.6 || ^10",
+ "squizlabs/php_codesniffer": "^3.13"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Slim\\Psr7\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Josh Lockhart",
+ "email": "hello@joshlockhart.com",
+ "homepage": "https://joshlockhart.com"
+ },
+ {
+ "name": "Andrew Smith",
+ "email": "a.smith@silentworks.co.uk",
+ "homepage": "https://silentworks.co.uk"
+ },
+ {
+ "name": "Rob Allen",
+ "email": "rob@akrabat.com",
+ "homepage": "https://akrabat.com"
+ },
+ {
+ "name": "Pierre Berube",
+ "email": "pierre@lgse.com",
+ "homepage": "https://www.lgse.com"
+ }
+ ],
+ "description": "Strict PSR-7 implementation",
+ "homepage": "https://www.slimframework.com",
+ "keywords": [
+ "http",
+ "psr-7",
+ "psr7"
+ ],
+ "support": {
+ "issues": "https://github.com/slimphp/Slim-Psr7/issues",
+ "source": "https://github.com/slimphp/Slim-Psr7/tree/1.8.0"
+ },
+ "time": "2025-11-02T17:51:19+00:00"
+ },
+ {
+ "name": "slim/slim",
+ "version": "4.15.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/slimphp/Slim.git",
+ "reference": "887893516557506f254d950425ce7f5387a26970"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/slimphp/Slim/zipball/887893516557506f254d950425ce7f5387a26970",
+ "reference": "887893516557506f254d950425ce7f5387a26970",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "nikic/fast-route": "^1.3",
+ "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
+ "psr/container": "^1.0 || ^2.0",
+ "psr/http-factory": "^1.1",
+ "psr/http-message": "^1.1 || ^2.0",
+ "psr/http-server-handler": "^1.0",
+ "psr/http-server-middleware": "^1.0",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "adriansuter/php-autoload-override": "^1.4 || ^2",
+ "ext-simplexml": "*",
+ "guzzlehttp/psr7": "^2.6",
+ "httpsoft/http-message": "^1.1",
+ "httpsoft/http-server-request": "^1.1",
+ "laminas/laminas-diactoros": "^2.17 || ^3",
+ "nyholm/psr7": "^1.8",
+ "nyholm/psr7-server": "^1.1",
+ "phpspec/prophecy": "^1.19",
+ "phpspec/prophecy-phpunit": "^2.1",
+ "phpstan/phpstan": "^1 || ^2",
+ "phpunit/phpunit": "^9.6 || ^10 || ^11 || ^12",
+ "slim/http": "^1.3",
+ "slim/psr7": "^1.6",
+ "squizlabs/php_codesniffer": "^3.10",
+ "vimeo/psalm": "^5 || ^6"
+ },
+ "suggest": {
+ "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware",
+ "ext-xml": "Needed to support XML format in BodyParsingMiddleware",
+ "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim",
+ "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Slim\\": "Slim"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Josh Lockhart",
+ "email": "hello@joshlockhart.com",
+ "homepage": "https://joshlockhart.com"
+ },
+ {
+ "name": "Andrew Smith",
+ "email": "a.smith@silentworks.co.uk",
+ "homepage": "https://silentworks.co.uk"
+ },
+ {
+ "name": "Rob Allen",
+ "email": "rob@akrabat.com",
+ "homepage": "https://akrabat.com"
+ },
+ {
+ "name": "Pierre Berube",
+ "email": "pierre@lgse.com",
+ "homepage": "https://www.lgse.com"
+ },
+ {
+ "name": "Gabriel Manricks",
+ "email": "gmanricks@me.com",
+ "homepage": "http://gabrielmanricks.com"
+ }
+ ],
+ "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs",
+ "homepage": "https://www.slimframework.com",
+ "keywords": [
+ "api",
+ "framework",
+ "micro",
+ "router"
+ ],
+ "support": {
+ "docs": "https://www.slimframework.com/docs/v4/",
+ "forum": "https://discourse.slimframework.com/",
+ "irc": "irc://irc.freenode.net:6667/slimphp",
+ "issues": "https://github.com/slimphp/Slim/issues",
+ "rss": "https://www.slimframework.com/blog/feed.rss",
+ "slack": "https://slimphp.slack.com/",
+ "source": "https://github.com/slimphp/Slim",
+ "wiki": "https://github.com/slimphp/Slim/wiki"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/slimphp",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/slim/slim",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-11-21T12:23:44+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": "^8.1"
+ },
+ "platform-dev": {},
+ "plugin-api-version": "2.9.0"
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..28e6f2b
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,18 @@
+version: '3.7'
+
+volumes:
+ logs:
+ driver: local
+
+services:
+ slim:
+ image: php:7-alpine
+ working_dir: /var/www
+ command: php -S 0.0.0.0:8080 -t public
+ environment:
+ docker: "true"
+ ports:
+ - "8080:8080"
+ volumes:
+ - .:/var/www
+ - logs:/var/www/logs
diff --git a/logs/README.md b/logs/README.md
new file mode 100644
index 0000000..d4a602e
--- /dev/null
+++ b/logs/README.md
@@ -0,0 +1 @@
+Your Slim Framework application's log files will be written to this directory.
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..e5c3d04
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,17 @@
+
+
+ Slim coding standard
+
+
+
+
+
+
+
+
+
+
+
+ src
+ tests
+
\ No newline at end of file
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000..2e09595
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,4 @@
+parameters:
+ level: 4
+ paths:
+ - src
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..f830400
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,27 @@
+
+
+
+ ./tests/
+
+
+
+
+ ./src/
+
+
+
diff --git a/public/.htaccess b/public/.htaccess
new file mode 100644
index 0000000..11b28de
--- /dev/null
+++ b/public/.htaccess
@@ -0,0 +1,4 @@
+RewriteEngine On
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule ^ index.php [QSA,L]
\ No newline at end of file
diff --git a/public/index.php b/public/index.php
new file mode 100644
index 0000000..2a97b97
--- /dev/null
+++ b/public/index.php
@@ -0,0 +1,50 @@
+addErrorMiddleware(true, true, true);
+
+// CORS Middleware
+$app->add(function (Request $request, $handler) {
+ $response = $handler->handle($request);
+ return $response
+ ->withHeader('Access-Control-Allow-Origin', '*')
+ ->withHeader('Access-Control-Allow-Headers', 'Content-Type')
+ ->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
+});
+
+// API Documentation
+$app->get('/', function (Request $request, Response $response) {
+ $docs = [
+ 'service' => 'MTEX Mock API',
+ 'version' => '1.0.0',
+ 'description' => 'Simple REST API providing sample JSON data',
+ 'endpoints' => [
+ 'GET /' => 'API documentation',
+ 'GET /users' => 'List all users',
+ 'GET /users/{id}' => 'Get user by ID',
+ 'GET /posts' => 'List all posts',
+ 'GET /posts/{id}' => 'Get post by ID',
+ 'GET /products' => 'List all products',
+ 'GET /products/{id}' => 'Get product by ID',
+ ],
+ 'examples' => [
+ 'https://mock.mtex.dev/users',
+ 'https://mock.mtex.dev/users/1',
+ 'https://mock.mtex.dev/posts',
+ 'https://mock.mtex.dev/products',
+ ],
+ ];
+
+ $response->getBody()->write(json_encode($docs, JSON_PRETTY_PRINT));
+ return $response->withHeader('Content-Type', 'application/json');
+});
+
+require __DIR__ . '/../src/routes.php';
+
+$app->run();
\ No newline at end of file
diff --git a/src/Application/Actions/Action.php b/src/Application/Actions/Action.php
new file mode 100644
index 0000000..1021c20
--- /dev/null
+++ b/src/Application/Actions/Action.php
@@ -0,0 +1,92 @@
+logger = $logger;
+ }
+
+ /**
+ * @throws HttpNotFoundException
+ * @throws HttpBadRequestException
+ */
+ public function __invoke(Request $request, Response $response, array $args): Response
+ {
+ $this->request = $request;
+ $this->response = $response;
+ $this->args = $args;
+
+ try {
+ return $this->action();
+ } catch (DomainRecordNotFoundException $e) {
+ throw new HttpNotFoundException($this->request, $e->getMessage());
+ }
+ }
+
+ /**
+ * @throws DomainRecordNotFoundException
+ * @throws HttpBadRequestException
+ */
+ abstract protected function action(): Response;
+
+ /**
+ * @return array|object
+ */
+ protected function getFormData()
+ {
+ return $this->request->getParsedBody();
+ }
+
+ /**
+ * @return mixed
+ * @throws HttpBadRequestException
+ */
+ protected function resolveArg(string $name)
+ {
+ if (!isset($this->args[$name])) {
+ throw new HttpBadRequestException($this->request, "Could not resolve argument `{$name}`.");
+ }
+
+ return $this->args[$name];
+ }
+
+ /**
+ * @param array|object|null $data
+ */
+ protected function respondWithData($data = null, int $statusCode = 200): Response
+ {
+ $payload = new ActionPayload($statusCode, $data);
+
+ return $this->respond($payload);
+ }
+
+ protected function respond(ActionPayload $payload): Response
+ {
+ $json = json_encode($payload, JSON_PRETTY_PRINT);
+ $this->response->getBody()->write($json);
+
+ return $this->response
+ ->withHeader('Content-Type', 'application/json')
+ ->withStatus($payload->getStatusCode());
+ }
+}
diff --git a/src/Application/Actions/ActionError.php b/src/Application/Actions/ActionError.php
new file mode 100644
index 0000000..3a81cc5
--- /dev/null
+++ b/src/Application/Actions/ActionError.php
@@ -0,0 +1,61 @@
+type = $type;
+ $this->description = $description;
+ }
+
+ public function getType(): string
+ {
+ return $this->type;
+ }
+
+ public function setType(string $type): self
+ {
+ $this->type = $type;
+ return $this;
+ }
+
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ public function setDescription(?string $description = null): self
+ {
+ $this->description = $description;
+ return $this;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize(): array
+ {
+ return [
+ 'type' => $this->type,
+ 'description' => $this->description,
+ ];
+ }
+}
diff --git a/src/Application/Actions/ActionPayload.php b/src/Application/Actions/ActionPayload.php
new file mode 100644
index 0000000..cbf5110
--- /dev/null
+++ b/src/Application/Actions/ActionPayload.php
@@ -0,0 +1,63 @@
+statusCode = $statusCode;
+ $this->data = $data;
+ $this->error = $error;
+ }
+
+ public function getStatusCode(): int
+ {
+ return $this->statusCode;
+ }
+
+ /**
+ * @return array|null|object
+ */
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ public function getError(): ?ActionError
+ {
+ return $this->error;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize(): array
+ {
+ $payload = [
+ 'statusCode' => $this->statusCode,
+ ];
+
+ if ($this->data !== null) {
+ $payload['data'] = $this->data;
+ } elseif ($this->error !== null) {
+ $payload['error'] = $this->error;
+ }
+
+ return $payload;
+ }
+}
diff --git a/src/Application/Actions/User/ListUsersAction.php b/src/Application/Actions/User/ListUsersAction.php
new file mode 100644
index 0000000..7ca74d7
--- /dev/null
+++ b/src/Application/Actions/User/ListUsersAction.php
@@ -0,0 +1,22 @@
+userRepository->findAll();
+
+ $this->logger->info("Users list was viewed.");
+
+ return $this->respondWithData($users);
+ }
+}
diff --git a/src/Application/Actions/User/UserAction.php b/src/Application/Actions/User/UserAction.php
new file mode 100644
index 0000000..4ea2d8c
--- /dev/null
+++ b/src/Application/Actions/User/UserAction.php
@@ -0,0 +1,20 @@
+userRepository = $userRepository;
+ }
+}
diff --git a/src/Application/Actions/User/ViewUserAction.php b/src/Application/Actions/User/ViewUserAction.php
new file mode 100644
index 0000000..711cf0a
--- /dev/null
+++ b/src/Application/Actions/User/ViewUserAction.php
@@ -0,0 +1,23 @@
+resolveArg('id');
+ $user = $this->userRepository->findUserOfId($userId);
+
+ $this->logger->info("User of id `${userId}` was viewed.");
+
+ return $this->respondWithData($user);
+ }
+}
diff --git a/src/Application/Handlers/HttpErrorHandler.php b/src/Application/Handlers/HttpErrorHandler.php
new file mode 100644
index 0000000..64e02b1
--- /dev/null
+++ b/src/Application/Handlers/HttpErrorHandler.php
@@ -0,0 +1,69 @@
+exception;
+ $statusCode = 500;
+ $error = new ActionError(
+ ActionError::SERVER_ERROR,
+ 'An internal error has occurred while processing your request.'
+ );
+
+ if ($exception instanceof HttpException) {
+ $statusCode = $exception->getCode();
+ $error->setDescription($exception->getMessage());
+
+ if ($exception instanceof HttpNotFoundException) {
+ $error->setType(ActionError::RESOURCE_NOT_FOUND);
+ } elseif ($exception instanceof HttpMethodNotAllowedException) {
+ $error->setType(ActionError::NOT_ALLOWED);
+ } elseif ($exception instanceof HttpUnauthorizedException) {
+ $error->setType(ActionError::UNAUTHENTICATED);
+ } elseif ($exception instanceof HttpForbiddenException) {
+ $error->setType(ActionError::INSUFFICIENT_PRIVILEGES);
+ } elseif ($exception instanceof HttpBadRequestException) {
+ $error->setType(ActionError::BAD_REQUEST);
+ } elseif ($exception instanceof HttpNotImplementedException) {
+ $error->setType(ActionError::NOT_IMPLEMENTED);
+ }
+ }
+
+ if (
+ !($exception instanceof HttpException)
+ && $exception instanceof Throwable
+ && $this->displayErrorDetails
+ ) {
+ $error->setDescription($exception->getMessage());
+ }
+
+ $payload = new ActionPayload($statusCode, null, $error);
+ $encodedPayload = json_encode($payload, JSON_PRETTY_PRINT);
+
+ $response = $this->responseFactory->createResponse($statusCode);
+ $response->getBody()->write($encodedPayload);
+
+ return $response->withHeader('Content-Type', 'application/json');
+ }
+}
diff --git a/src/Application/Handlers/ShutdownHandler.php b/src/Application/Handlers/ShutdownHandler.php
new file mode 100644
index 0000000..dc54b27
--- /dev/null
+++ b/src/Application/Handlers/ShutdownHandler.php
@@ -0,0 +1,74 @@
+request = $request;
+ $this->errorHandler = $errorHandler;
+ $this->displayErrorDetails = $displayErrorDetails;
+ }
+
+ public function __invoke()
+ {
+ $error = error_get_last();
+ if ($error) {
+ $errorFile = $error['file'];
+ $errorLine = $error['line'];
+ $errorMessage = $error['message'];
+ $errorType = $error['type'];
+ $message = 'An error while processing your request. Please try again later.';
+
+ if ($this->displayErrorDetails) {
+ switch ($errorType) {
+ case E_USER_ERROR:
+ $message = "FATAL ERROR: {$errorMessage}. ";
+ $message .= " on line {$errorLine} in file {$errorFile}.";
+ break;
+
+ case E_USER_WARNING:
+ $message = "WARNING: {$errorMessage}";
+ break;
+
+ case E_USER_NOTICE:
+ $message = "NOTICE: {$errorMessage}";
+ break;
+
+ default:
+ $message = "ERROR: {$errorMessage}";
+ $message .= " on line {$errorLine} in file {$errorFile}.";
+ break;
+ }
+ }
+
+ $exception = new HttpInternalServerErrorException($this->request, $message);
+ $response = $this->errorHandler->__invoke(
+ $this->request,
+ $exception,
+ $this->displayErrorDetails,
+ false,
+ false,
+ );
+
+ $responseEmitter = new ResponseEmitter();
+ $responseEmitter->emit($response);
+ }
+ }
+}
diff --git a/src/Application/Middleware/SessionMiddleware.php b/src/Application/Middleware/SessionMiddleware.php
new file mode 100644
index 0000000..d7dc923
--- /dev/null
+++ b/src/Application/Middleware/SessionMiddleware.php
@@ -0,0 +1,26 @@
+withAttribute('session', $_SESSION);
+ }
+
+ return $handler->handle($request);
+ }
+}
diff --git a/src/Application/ResponseEmitter/ResponseEmitter.php b/src/Application/ResponseEmitter/ResponseEmitter.php
new file mode 100644
index 0000000..8a27ae1
--- /dev/null
+++ b/src/Application/ResponseEmitter/ResponseEmitter.php
@@ -0,0 +1,38 @@
+withHeader('Access-Control-Allow-Credentials', 'true')
+ ->withHeader('Access-Control-Allow-Origin', $origin)
+ ->withHeader(
+ 'Access-Control-Allow-Headers',
+ 'X-Requested-With, Content-Type, Accept, Origin, Authorization',
+ )
+ ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
+ ->withHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
+ ->withAddedHeader('Cache-Control', 'post-check=0, pre-check=0')
+ ->withHeader('Pragma', 'no-cache');
+
+ if (ob_get_contents()) {
+ ob_clean();
+ }
+
+ parent::emit($response);
+ }
+}
diff --git a/src/Application/Settings/Settings.php b/src/Application/Settings/Settings.php
new file mode 100644
index 0000000..6da55e4
--- /dev/null
+++ b/src/Application/Settings/Settings.php
@@ -0,0 +1,23 @@
+settings = $settings;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function get(string $key = '')
+ {
+ return (empty($key)) ? $this->settings : $this->settings[$key];
+ }
+}
diff --git a/src/Application/Settings/SettingsInterface.php b/src/Application/Settings/SettingsInterface.php
new file mode 100644
index 0000000..557d98b
--- /dev/null
+++ b/src/Application/Settings/SettingsInterface.php
@@ -0,0 +1,14 @@
+id = $id;
+ $this->username = strtolower($username);
+ $this->firstName = ucfirst($firstName);
+ $this->lastName = ucfirst($lastName);
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getUsername(): string
+ {
+ return $this->username;
+ }
+
+ public function getFirstName(): string
+ {
+ return $this->firstName;
+ }
+
+ public function getLastName(): string
+ {
+ return $this->lastName;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize(): array
+ {
+ return [
+ 'id' => $this->id,
+ 'username' => $this->username,
+ 'firstName' => $this->firstName,
+ 'lastName' => $this->lastName,
+ ];
+ }
+}
diff --git a/src/Domain/User/UserNotFoundException.php b/src/Domain/User/UserNotFoundException.php
new file mode 100644
index 0000000..ae3d11e
--- /dev/null
+++ b/src/Domain/User/UserNotFoundException.php
@@ -0,0 +1,12 @@
+users = $users ?? [
+ 1 => new User(1, 'bill.gates', 'Bill', 'Gates'),
+ 2 => new User(2, 'steve.jobs', 'Steve', 'Jobs'),
+ 3 => new User(3, 'mark.zuckerberg', 'Mark', 'Zuckerberg'),
+ 4 => new User(4, 'evan.spiegel', 'Evan', 'Spiegel'),
+ 5 => new User(5, 'jack.dorsey', 'Jack', 'Dorsey'),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function findAll(): array
+ {
+ return array_values($this->users);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function findUserOfId(int $id): User
+ {
+ if (!isset($this->users[$id])) {
+ throw new UserNotFoundException();
+ }
+
+ return $this->users[$id];
+ }
+}
diff --git a/src/data/posts.php b/src/data/posts.php
new file mode 100644
index 0000000..446f605
--- /dev/null
+++ b/src/data/posts.php
@@ -0,0 +1,28 @@
+ 1,
+ 'userId' => 1,
+ 'title' => 'Getting Started with Slim PHP',
+ 'body' => 'Slim is a PHP micro framework that helps you write simple web applications and APIs.',
+ 'tags' => ['php', 'slim', 'tutorial'],
+ 'published' => true,
+ ],
+ [
+ 'id' => 2,
+ 'userId' => 2,
+ 'title' => 'REST API Best Practices',
+ 'body' => 'Learn how to design clean and efficient REST APIs.',
+ 'tags' => ['api', 'rest', 'best-practices'],
+ 'published' => true,
+ ],
+ [
+ 'id' => 3,
+ 'userId' => 1,
+ 'title' => 'Mock Data for Testing',
+ 'body' => 'Why using mock data is essential for rapid development.',
+ 'tags' => ['testing', 'mock', 'development'],
+ 'published' => false,
+ ],
+];
\ No newline at end of file
diff --git a/src/data/products.php b/src/data/products.php
new file mode 100644
index 0000000..c7cbf79
--- /dev/null
+++ b/src/data/products.php
@@ -0,0 +1,36 @@
+ 1,
+ 'name' => 'Wireless Headphones',
+ 'price' => 79.99,
+ 'category' => 'Electronics',
+ 'inStock' => true,
+ 'rating' => 4.5,
+ ],
+ [
+ 'id' => 2,
+ 'name' => 'Coffee Maker',
+ 'price' => 49.99,
+ 'category' => 'Home & Kitchen',
+ 'inStock' => true,
+ 'rating' => 4.2,
+ ],
+ [
+ 'id' => 3,
+ 'name' => 'Running Shoes',
+ 'price' => 89.99,
+ 'category' => 'Sports',
+ 'inStock' => false,
+ 'rating' => 4.8,
+ ],
+ [
+ 'id' => 4,
+ 'name' => 'Desk Lamp',
+ 'price' => 29.99,
+ 'category' => 'Home & Office',
+ 'inStock' => true,
+ 'rating' => 4.0,
+ ],
+];
\ No newline at end of file
diff --git a/src/data/users.php b/src/data/users.php
new file mode 100644
index 0000000..322f991
--- /dev/null
+++ b/src/data/users.php
@@ -0,0 +1,32 @@
+ 1,
+ 'name' => 'Alice Johnson',
+ 'email' => 'alice@example.mtex.dev',
+ 'role' => 'admin',
+ 'active' => true,
+ ],
+ [
+ 'id' => 2,
+ 'name' => 'Bob Smith',
+ 'email' => 'bob@example.mtex.dev',
+ 'role' => 'user',
+ 'active' => true,
+ ],
+ [
+ 'id' => 3,
+ 'name' => 'Charlie Brown',
+ 'email' => 'charlie@example.mtex.dev',
+ 'role' => 'user',
+ 'active' => false,
+ ],
+ [
+ 'id' => 4,
+ 'name' => 'Diana Prince',
+ 'email' => 'diana@example.mtex.dev',
+ 'role' => 'moderator',
+ 'active' => true,
+ ],
+];
\ No newline at end of file
diff --git a/src/routes.php b/src/routes.php
new file mode 100644
index 0000000..33176d0
--- /dev/null
+++ b/src/routes.php
@@ -0,0 +1,86 @@
+get('/users', function (Request $request, Response $response) {
+ $users = require __DIR__ . '/data/users.php';
+ $response->getBody()->write(json_encode($users, JSON_PRETTY_PRINT));
+ return $response->withHeader('Content-Type', 'application/json');
+});
+
+$app->get('/users/{id}', function (Request $request, Response $response, $args) {
+ $users = require __DIR__ . '/data/users.php';
+ $id = (int) $args['id'];
+
+ $user = array_values(array_filter($users, fn($u) => $u['id'] === $id));
+
+ if (empty($user)) {
+ $error = ['error' => 'User not found'];
+ $response->getBody()->write(json_encode($error));
+ return $response
+ ->withHeader('Content-Type', 'application/json')
+ ->withStatus(404);
+ }
+
+ $response->getBody()->write(json_encode($user[0], JSON_PRETTY_PRINT));
+ return $response->withHeader('Content-Type', 'application/json');
+});
+
+// Posts Routes
+$app->get('/posts', function (Request $request, Response $response) {
+ $posts = require __DIR__ . '/data/posts.php';
+ $response->getBody()->write(json_encode($posts, JSON_PRETTY_PRINT));
+ return $response->withHeader('Content-Type', 'application/json');
+});
+
+$app->get('/posts/{id}', function (Request $request, Response $response, $args) {
+ $posts = require __DIR__ . '/data/posts.php';
+ $id = (int) $args['id'];
+
+ $post = array_values(array_filter($posts, fn($p) => $p['id'] === $id));
+
+ if (empty($post)) {
+ $error = ['error' => 'Post not found'];
+ $response->getBody()->write(json_encode($error));
+ return $response
+ ->withHeader('Content-Type', 'application/json')
+ ->withStatus(404);
+ }
+
+ $response->getBody()->write(json_encode($post[0], JSON_PRETTY_PRINT));
+ return $response->withHeader('Content-Type', 'application/json');
+});
+
+// Products Routes
+$app->get('/products', function (Request $request, Response $response) {
+ $products = require __DIR__ . '/data/products.php';
+ $response->getBody()->write(json_encode($products, JSON_PRETTY_PRINT));
+ return $response->withHeader('Content-Type', 'application/json');
+});
+
+$app->get(
+ '/products/{id}',
+ function (Request $request, Response $response, $args) {
+ $products = require __DIR__ . '/data/products.php';
+ $id = (int) $args['id'];
+
+ $product = array_values(
+ array_filter($products, fn($p) => $p['id'] === $id)
+ );
+
+ if (empty($product)) {
+ $error = ['error' => 'Product not found'];
+ $response->getBody()->write(json_encode($error));
+ return $response
+ ->withHeader('Content-Type', 'application/json')
+ ->withStatus(404);
+ }
+
+ $response->getBody()->write(
+ json_encode($product[0], JSON_PRETTY_PRINT)
+ );
+ return $response->withHeader('Content-Type', 'application/json');
+ }
+);
\ No newline at end of file
diff --git a/tests/Application/Actions/ActionTest.php b/tests/Application/Actions/ActionTest.php
new file mode 100644
index 0000000..9881c68
--- /dev/null
+++ b/tests/Application/Actions/ActionTest.php
@@ -0,0 +1,79 @@
+getAppInstance();
+ $container = $app->getContainer();
+ $logger = $container->get(LoggerInterface::class);
+
+ $testAction = new class ($logger) extends Action {
+ public function __construct(
+ LoggerInterface $loggerInterface
+ ) {
+ parent::__construct($loggerInterface);
+ }
+
+ public function action(): Response
+ {
+ return $this->respond(
+ new ActionPayload(
+ 202,
+ [
+ 'willBeDoneAt' => (new DateTimeImmutable())->format(DateTimeImmutable::ATOM)
+ ]
+ )
+ );
+ }
+ };
+
+ $app->get('/test-action-response-code', $testAction);
+ $request = $this->createRequest('GET', '/test-action-response-code');
+ $response = $app->handle($request);
+
+ $this->assertEquals(202, $response->getStatusCode());
+ }
+
+ public function testActionSetsHttpCodeRespondData()
+ {
+ $app = $this->getAppInstance();
+ $container = $app->getContainer();
+ $logger = $container->get(LoggerInterface::class);
+
+ $testAction = new class ($logger) extends Action {
+ public function __construct(
+ LoggerInterface $loggerInterface
+ ) {
+ parent::__construct($loggerInterface);
+ }
+
+ public function action(): Response
+ {
+ return $this->respondWithData(
+ [
+ 'willBeDoneAt' => (new DateTimeImmutable())->format(DateTimeImmutable::ATOM)
+ ],
+ 202
+ );
+ }
+ };
+
+ $app->get('/test-action-response-code', $testAction);
+ $request = $this->createRequest('GET', '/test-action-response-code');
+ $response = $app->handle($request);
+
+ $this->assertEquals(202, $response->getStatusCode());
+ }
+}
diff --git a/tests/Application/Actions/User/ListUserActionTest.php b/tests/Application/Actions/User/ListUserActionTest.php
new file mode 100644
index 0000000..fcf3222
--- /dev/null
+++ b/tests/Application/Actions/User/ListUserActionTest.php
@@ -0,0 +1,41 @@
+getAppInstance();
+
+ /** @var Container $container */
+ $container = $app->getContainer();
+
+ $user = new User(1, 'bill.gates', 'Bill', 'Gates');
+
+ $userRepositoryProphecy = $this->prophesize(UserRepository::class);
+ $userRepositoryProphecy
+ ->findAll()
+ ->willReturn([$user])
+ ->shouldBeCalledOnce();
+
+ $container->set(UserRepository::class, $userRepositoryProphecy->reveal());
+
+ $request = $this->createRequest('GET', '/users');
+ $response = $app->handle($request);
+
+ $payload = (string) $response->getBody();
+ $expectedPayload = new ActionPayload(200, [$user]);
+ $serializedPayload = json_encode($expectedPayload, JSON_PRETTY_PRINT);
+
+ $this->assertEquals($serializedPayload, $payload);
+ }
+}
diff --git a/tests/Application/Actions/User/ViewUserActionTest.php b/tests/Application/Actions/User/ViewUserActionTest.php
new file mode 100644
index 0000000..785b6c6
--- /dev/null
+++ b/tests/Application/Actions/User/ViewUserActionTest.php
@@ -0,0 +1,80 @@
+getAppInstance();
+
+ /** @var Container $container */
+ $container = $app->getContainer();
+
+ $user = new User(1, 'bill.gates', 'Bill', 'Gates');
+
+ $userRepositoryProphecy = $this->prophesize(UserRepository::class);
+ $userRepositoryProphecy
+ ->findUserOfId(1)
+ ->willReturn($user)
+ ->shouldBeCalledOnce();
+
+ $container->set(UserRepository::class, $userRepositoryProphecy->reveal());
+
+ $request = $this->createRequest('GET', '/users/1');
+ $response = $app->handle($request);
+
+ $payload = (string) $response->getBody();
+ $expectedPayload = new ActionPayload(200, $user);
+ $serializedPayload = json_encode($expectedPayload, JSON_PRETTY_PRINT);
+
+ $this->assertEquals($serializedPayload, $payload);
+ }
+
+ public function testActionThrowsUserNotFoundException()
+ {
+ $app = $this->getAppInstance();
+
+ $callableResolver = $app->getCallableResolver();
+ $responseFactory = $app->getResponseFactory();
+
+ $errorHandler = new HttpErrorHandler($callableResolver, $responseFactory);
+ $errorMiddleware = new ErrorMiddleware($callableResolver, $responseFactory, true, false, false);
+ $errorMiddleware->setDefaultErrorHandler($errorHandler);
+
+ $app->add($errorMiddleware);
+
+ /** @var Container $container */
+ $container = $app->getContainer();
+
+ $userRepositoryProphecy = $this->prophesize(UserRepository::class);
+ $userRepositoryProphecy
+ ->findUserOfId(1)
+ ->willThrow(new UserNotFoundException())
+ ->shouldBeCalledOnce();
+
+ $container->set(UserRepository::class, $userRepositoryProphecy->reveal());
+
+ $request = $this->createRequest('GET', '/users/1');
+ $response = $app->handle($request);
+
+ $payload = (string) $response->getBody();
+ $expectedError = new ActionError(ActionError::RESOURCE_NOT_FOUND, 'The user you requested does not exist.');
+ $expectedPayload = new ActionPayload(404, null, $expectedError);
+ $serializedPayload = json_encode($expectedPayload, JSON_PRETTY_PRINT);
+
+ $this->assertEquals($serializedPayload, $payload);
+ }
+}
diff --git a/tests/Domain/User/UserTest.php b/tests/Domain/User/UserTest.php
new file mode 100644
index 0000000..36f4925
--- /dev/null
+++ b/tests/Domain/User/UserTest.php
@@ -0,0 +1,60 @@
+assertEquals($id, $user->getId());
+ $this->assertEquals($username, $user->getUsername());
+ $this->assertEquals($firstName, $user->getFirstName());
+ $this->assertEquals($lastName, $user->getLastName());
+ }
+
+ /**
+ * @dataProvider userProvider
+ * @param int $id
+ * @param string $username
+ * @param string $firstName
+ * @param string $lastName
+ */
+ public function testJsonSerialize(int $id, string $username, string $firstName, string $lastName)
+ {
+ $user = new User($id, $username, $firstName, $lastName);
+
+ $expectedPayload = json_encode([
+ 'id' => $id,
+ 'username' => $username,
+ 'firstName' => $firstName,
+ 'lastName' => $lastName,
+ ]);
+
+ $this->assertEquals($expectedPayload, json_encode($user));
+ }
+}
diff --git a/tests/Infrastructure/Persistence/User/InMemoryUserRepositoryTest.php b/tests/Infrastructure/Persistence/User/InMemoryUserRepositoryTest.php
new file mode 100644
index 0000000..6b51b0c
--- /dev/null
+++ b/tests/Infrastructure/Persistence/User/InMemoryUserRepositoryTest.php
@@ -0,0 +1,53 @@
+ $user]);
+
+ $this->assertEquals([$user], $userRepository->findAll());
+ }
+
+ public function testFindAllUsersByDefault()
+ {
+ $users = [
+ 1 => new User(1, 'bill.gates', 'Bill', 'Gates'),
+ 2 => new User(2, 'steve.jobs', 'Steve', 'Jobs'),
+ 3 => new User(3, 'mark.zuckerberg', 'Mark', 'Zuckerberg'),
+ 4 => new User(4, 'evan.spiegel', 'Evan', 'Spiegel'),
+ 5 => new User(5, 'jack.dorsey', 'Jack', 'Dorsey'),
+ ];
+
+ $userRepository = new InMemoryUserRepository();
+
+ $this->assertEquals(array_values($users), $userRepository->findAll());
+ }
+
+ public function testFindUserOfId()
+ {
+ $user = new User(1, 'bill.gates', 'Bill', 'Gates');
+
+ $userRepository = new InMemoryUserRepository([1 => $user]);
+
+ $this->assertEquals($user, $userRepository->findUserOfId(1));
+ }
+
+ public function testFindUserOfIdThrowsNotFoundException()
+ {
+ $userRepository = new InMemoryUserRepository([]);
+ $this->expectException(UserNotFoundException::class);
+ $userRepository->findUserOfId(1);
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..a55f09d
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,90 @@
+build();
+
+ // Instantiate the app
+ AppFactory::setContainer($container);
+ $app = AppFactory::create();
+
+ // Register middleware
+ $middleware = require __DIR__ . '/../app/middleware.php';
+ $middleware($app);
+
+ // Register routes
+ $routes = require __DIR__ . '/../app/routes.php';
+ $routes($app);
+
+ return $app;
+ }
+
+ /**
+ * @param string $method
+ * @param string $path
+ * @param array $headers
+ * @param array $cookies
+ * @param array $serverParams
+ * @return Request
+ */
+ protected function createRequest(
+ string $method,
+ string $path,
+ array $headers = ['HTTP_ACCEPT' => 'application/json'],
+ array $cookies = [],
+ array $serverParams = []
+ ): Request {
+ $uri = new Uri('', '', 80, $path);
+ $handle = fopen('php://temp', 'w+');
+ $stream = (new StreamFactory())->createStreamFromResource($handle);
+
+ $h = new Headers();
+ foreach ($headers as $name => $value) {
+ $h->addHeader($name, $value);
+ }
+
+ return new SlimRequest($method, $uri, $h, $cookies, $serverParams, $stream);
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..d21c14d
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,3 @@
+