- Add Dockerfile, docker-compose.yml, .dockerignore - Add instance() binding for concrete objects in App container - Add make() for auto-wiring class dependencies - Add clear() for test isolation - Document new container features in MVC skillAGENT_WORK
| @@ -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. | Do not call `Request::capture()` inside action bodies. Declare the parameter instead. | ||||
| --- | --- | ||||
| @@ -0,0 +1,4 @@ | |||||
| .git | |||||
| .ai | |||||
| vendor | |||||
| database/app.sqlite | |||||
| @@ -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"] | |||||
| @@ -5,21 +5,55 @@ declare(strict_types=1); | |||||
| namespace Core; | namespace Core; | ||||
| use Exception; | use Exception; | ||||
| use ReflectionClass; | |||||
| use ReflectionFunction; | use ReflectionFunction; | ||||
| use ReflectionFunctionAbstract; | |||||
| use ReflectionMethod; | use ReflectionMethod; | ||||
| class App | class App | ||||
| { | { | ||||
| protected array $bindings = []; | protected array $bindings = []; | ||||
| protected array $instances = []; | |||||
| public function bind(string $name, mixed $value): void | public function bind(string $name, mixed $value): void | ||||
| { | { | ||||
| $this->bindings[$name] = $value; | $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 | 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 | public function call(callable|array|string $handler, array $parameters = []): mixed | ||||
| @@ -72,8 +106,20 @@ class App | |||||
| if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { | if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { | ||||
| $typeName = $type->getName(); | $typeName = $type->getName(); | ||||
| if (array_key_exists($typeName, $this->instances)) { | |||||
| $args[] = $this->instances[$typeName]; | |||||
| continue; | |||||
| } | |||||
| if (array_key_exists($typeName, $this->bindings)) { | 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; | continue; | ||||
| } | } | ||||
| } | } | ||||
| @@ -83,4 +129,26 @@ class App | |||||
| return $args; | 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); | |||||
| } | |||||
| } | } | ||||
| @@ -0,0 +1,9 @@ | |||||
| services: | |||||
| app: | |||||
| build: . | |||||
| ports: | |||||
| - "8080:80" | |||||
| volumes: | |||||
| - .:/var/www/html | |||||
| environment: | |||||
| APP_DEBUG: "true" | |||||
| @@ -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 | |||||
| @@ -0,0 +1,13 @@ | |||||
| <VirtualHost *:80> | |||||
| ServerName localhost | |||||
| DocumentRoot /var/www/html/public | |||||
| <Directory /var/www/html/public> | |||||
| Options -Indexes +FollowSymLinks | |||||
| AllowOverride All | |||||
| Require all granted | |||||
| </Directory> | |||||
| ErrorLog ${APACHE_LOG_DIR}/error.log | |||||
| CustomLog ${APACHE_LOG_DIR}/access.log combined | |||||
| </VirtualHost> | |||||
Powered by TurnKey Linux.