diff --git a/.ai/skills/mvc/SKILL.md b/.ai/skills/mvc/SKILL.md index cdd606a..da7750e 100644 --- a/.ai/skills/mvc/SKILL.md +++ b/.ai/skills/mvc/SKILL.md @@ -154,6 +154,17 @@ public function index(Database $db): Response } ``` +**Binding concrete instances:** Use `$app->instance($name, $obj)` to bind a specific object by key. Instances take precedence over bindings when resolving. + +**Auto-wiring:** Use `$app->make(SomeClass::class)` to resolve a class with its constructor dependencies injected automatically: + +```php +$repo = $app->make(EmployeeRepository::class); +// $app checks bindings first, then instantiates the class and resolves its constructor +``` + +**Test isolation:** Call `$app->clear()` to reset all bindings and instances between test runs. + Do not call `Request::capture()` inside action bodies. Declare the parameter instead. --- diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f4a0755 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +.ai +vendor +database/app.sqlite diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0a6f431 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM php:8.5-apache + +# Install pdo_sqlite and enable mod_rewrite +RUN apt-get update \ + && apt-get install -y libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* \ + && docker-php-ext-install pdo_sqlite \ + && a2enmod rewrite + +# Install Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Configure Apache virtual host +COPY docker/vhost.conf /etc/apache2/sites-available/000-default.conf +COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +WORKDIR /var/www/html + +# Copy application files +COPY . . + +# Generate autoloader (no external dependencies — just generates vendor/autoload.php) +RUN composer install --no-dev --optimize-autoloader --no-interaction + +# Create database directory and set correct permissions +RUN mkdir -p database \ + && chown -R www-data:www-data /var/www/html \ + && chmod 775 database + +EXPOSE 80 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/core/App.php b/core/App.php index 16e8ad1..67e85e3 100644 --- a/core/App.php +++ b/core/App.php @@ -5,21 +5,55 @@ declare(strict_types=1); namespace Core; use Exception; +use ReflectionClass; use ReflectionFunction; +use ReflectionFunctionAbstract; use ReflectionMethod; class App { protected array $bindings = []; + protected array $instances = []; + public function bind(string $name, mixed $value): void { $this->bindings[$name] = $value; } + public function instance(string $name, mixed $value): void + { + $this->instances[$name] = $value; + } + + public function make(string $className): object + { + if (isset($this->bindings[$className])) { + $binding = $this->bindings[$className]; + + if (is_string($binding) && is_a($binding, $className, true)) { + return $this->instantiate($binding); + } + + return $binding; + } + + if (isset($this->instances[$className])) { + return $this->instances[$className]; + } + + return $this->instantiate($className); + } + + public function clear(): void + { + $this->bindings = []; + $this->instances = []; + } + public function get(string $name): mixed { - return $this->bindings[$name] ?? null; + return $this->instances[$name] ?? $this->bindings[$name] ?? null; } public function call(callable|array|string $handler, array $parameters = []): mixed @@ -72,8 +106,20 @@ class App if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { $typeName = $type->getName(); + if (array_key_exists($typeName, $this->instances)) { + $args[] = $this->instances[$typeName]; + continue; + } + if (array_key_exists($typeName, $this->bindings)) { - $args[] = $this->bindings[$typeName]; + $binding = $this->bindings[$typeName]; + + if (is_string($binding) && is_a($binding, $typeName, true)) { + $args[] = $this->instantiate($binding); + continue; + } + + $args[] = $binding; continue; } } @@ -83,4 +129,26 @@ class App return $args; } + + /** + * @param class-string $className + */ + protected function instantiate(string $className): object + { + $reflection = new ReflectionClass($className); + + if (!$reflection->isInstantiable()) { + throw new Exception("Cannot instantiate {$className}: class is not instantiable."); + } + + $constructor = $reflection->getConstructor(); + + if ($constructor === null) { + return new $className(); + } + + $args = $this->resolveArgs($constructor, []); + + return $reflection->newInstanceArgs($args); + } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e0b15a3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + app: + build: . + ports: + - "8080:80" + volumes: + - .:/var/www/html + environment: + APP_DEBUG: "true" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..4b6dfc3 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +mkdir -p database +chmod 777 database + +composer install --no-interaction --quiet + +# Runs as root — migrations may create database/app.sqlite owned by root. +php scripts/migrate.php up + +# Fix ownership after migrations so www-data (Apache) can write. +# chmod works on Windows volume mounts even when chown is ignored by p9fs. +chmod 777 database +chmod 666 database/app.sqlite + +exec apache2-foreground diff --git a/docker/vhost.conf b/docker/vhost.conf new file mode 100644 index 0000000..829dd31 --- /dev/null +++ b/docker/vhost.conf @@ -0,0 +1,13 @@ + + ServerName localhost + DocumentRoot /var/www/html/public + + + Options -Indexes +FollowSymLinks + AllowOverride All + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined +