Skip to main content

loom-php

PHP language expertise for idiomatic, production-quality code.

Informations de source

Dépôt
cosmix/loom
Dernière activité de la source
25 septembre 2026 à 06:00
Langue détectée de SKILL.md
anglais
Étoiles
61
Forks
4

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
loom-php
description
PHP language expertise for idiomatic, production-quality code.
triggers
["php","composer","packagist","psr-4","phpunit","pest","laravel","eloquent","artisan","symfony","doctrine","phpstan","psalm","php-cs-fixer","pint","rector","octane","php-fpm"]
# PHP Language Expertise ## Overview Idiomatic, production-grade PHP 8.x: Composer, strict types, enums and readonly, the Laravel and Symfony essentials, and PHPUnit/Pest. Assumes competence; the value is in what is easy to get wrong (coercive typing, loose comparison, the by-reference `foreach` trap, state leaking in long-running workers, N+1 queries, unsafe `unserialize`) and in writing tests that loom's `phpunit` and `pest` adapters can select one at a time. ## Tooling **Composer** manages dependencies and generates the autoloader. | Task | Command | | --- | --- | | Add a dependency | `composer require guzzlehttp/guzzle` (updates `composer.json` and `composer.lock`) | | Add a dev dependency | `composer require --dev phpunit/phpunit` | | Install from the lock | `composer install` (CI: `--no-interaction --prefer-dist`) | | Production install | `composer install --no-dev --classmap-authoritative` | | Update one package | `composer update vendor/package --with-dependencies` | | Regenerate the autoloader | `composer dump-autoload` (after editing `autoload`) | | Check the manifest | `composer validate --strict` | | Known vulnerabilities | `composer audit` | - Commit `composer.lock` for applications. Add and remove packages with `composer require`/`composer remove`; the `autoload`, `config` and `scripts` sections are edited by hand. - `config.platform.php` pins the PHP version used for resolution, so a newer local PHP cannot lock packages the server cannot run. - PSR-4: one class per file, namespace prefix mapped to a directory; a class whose path does not match its namespace fails to autoload only when first used. ```json { "require": { "php": "^8.3" }, "autoload": { "psr-4": { "App\\": "src/" } }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } }, "config": { "platform": { "php": "8.3.0" }, "sort-packages": true }, "scripts": { "test": "phpunit", "analyse": "phpstan analyse" } } ``` **Format:** PHP-CS-Fixer (`vendor/bin/php-cs-fixer fix`), Laravel Pint (`vendor/bin/pint`, PHP-CS-Fixer with Laravel presets), or PHP_CodeSniffer (`phpcs`/`phpcbf`) with PSR-12 / PER Coding Style. **Static analysis:** PHPStan (`vendor/bin/phpstan analyse`; levels 0 to 10 in PHPStan 2, `max` is the highest) or Psalm; Larastan adds Laravel's magic. Adopting on legacy code, generate a baseline (`--generate-baseline`) and shrink it; never add new errors to it. Generics exist only in docblocks for these tools: `@param list<User>`, `@return array<string, int>`, `@template T`. **Upgrades:** Rector (`vendor/bin/rector process --dry-run`) automates version migrations and PHPUnit attribute conversion. **The gate:** ```bash composer validate --strict vendor/bin/php-cs-fixer fix --dry-run --diff # or: vendor/bin/pint --test vendor/bin/phpstan analyse vendor/bin/phpunit # or: vendor/bin/pest composer audit ``` ## Types and Strictness Start every file with `declare(strict_types=1);`. Without it scalar parameters coerce (`"42"` becomes `42`, `true` becomes `1`). The mode applies to calls made *from* the declaring file, so a strict library called from a non-strict file still receives coerced values. | Version | Type features | | --- | --- | | 8.0 | Union types `int\|string`, `mixed`, `static` return, constructor promotion, named arguments, `match`, nullsafe `?->`, `throw` as an expression, attributes | | 8.1 | Enums, readonly properties, `never`, intersection types `A&B`, `new` in initializers, callable syntax `strlen(...)`, fibers | | 8.2 | Readonly classes, DNF types `(A&B)\|null`, standalone `true`/`false`/`null`, dynamic properties deprecated | | 8.3 | Typed class constants, `#[\Override]`, `json_validate()` | | 8.4 | Property hooks, asymmetric visibility `public private(set)`, `new Foo()->method()` without wrapping parentheses; implicit nullable parameters (`Foo $x = null`) deprecated, write `?Foo $x = null` | ```php <?php declare(strict_types=1); namespace App\Spool; final class Writer { public function __construct( private readonly string $root, private readonly Clock $clock = new SystemClock(), // new in initializer (8.1) ) {} /** @param list<string> $lines */ public function write(string $jobId, array $lines): int { if (is_link($this->root)) { throw new SymlinkException("spool root is a symlink: {$this->root}"); } return $this->append($this->root . '/' . $jobId, $lines, $this->clock->now()); } } ``` ## Enums and Readonly - Pure enums (`enum Suit { case Hearts; ... }`) and backed enums (`enum Status: string`). `Status::from($v)` throws `ValueError` on an unknown value; `Status::tryFrom($v)` returns `null`. `Status::cases()` lists them. Enums take methods, constants and interfaces; they hold no state. - `match` compares with `===` and throws `UnhandledMatchError` when no arm matches. A `match` over an enum without `default` fails loudly when a case is added; keep it that way. - Readonly properties are written once, from inside the class, and must be typed. They are shallow: an object held in a readonly property stays mutable. Readonly classes (8.2) make every promoted and declared property readonly and forbid dynamic properties. - Modify a readonly value object by constructing a new one (`withCurrency()` returning `new self(...)`); 8.3 also allows reinitializing readonly properties inside `__clone`. - Property hooks (8.4) replace most getter/setter pairs; asymmetric visibility (`public private(set)`) gives public reads with private writes. ```php enum Status: string { case Pending = 'pending'; case Active = 'active'; case Suspended = 'suspended'; public function canLogIn(): bool { return match ($this) { self::Active => true, self::Pending, self::Suspended => false, }; } } $status = Status::tryFrom($input) ?? throw new \InvalidArgumentException("unknown status: {$input}"); final readonly class Money { public function __construct(public int $cents, public string $currency) {} public function add(self $other): self { if ($other->currency !== $this->currency) { throw new \LogicException('currency mismatch'); } return new self($this->cents + $other->cents, $this->currency); } } final class Account { public string $email { set => strtolower(trim($value)); // 8.4 set hook } public function __construct(public private(set) string $id, string $email) { $this->email = $email; } } ``` ## Errors and Exceptions - `Throwable` splits into `Error` (engine: `TypeError`, `ValueError`, `ArgumentCountError`, `UnhandledMatchError`, `DivisionByZeroError`) and `Exception`. Catch `Exception` subclasses for recoverable conditions; catch `Throwable` only in the top-level handler. - Give a library a marker interface (`interface SpoolException extends \Throwable {}`) and concrete classes extending SPL exceptions (`final class SymlinkException extends \RuntimeException implements SpoolException`). Callers catch the interface for everything or a class for one case. - Chain the cause: `throw new SpoolWriteException('write failed', previous: $e);`. - `json_decode($s, true, 512, JSON_THROW_ON_ERROR)` and `json_encode($v, JSON_THROW_ON_ERROR)`: without the flag, bad input returns `null`/`false` and the error hides in `json_last_error()`. - `preg_*` functions return `false` (or `null`) on a regex error such as backtrack-limit exhaustion; check the return and `preg_last_error_msg()`. - A `return` inside `finally` replaces both the `try` block's return value and any in-flight exception. - Never silence errors with `@`; the failure disappears from logs and the code carries on with a bad value. ## Concurrency and the Request Model - PHP-FPM is share-nothing: every request starts with fresh statics, singletons and globals, so per-request leaks vanish at the end of the request. - Long-running processes (Laravel Octane, RoadRunner, FrankenPHP worker mode, Swoole, queue workers, Messenger consumers) keep the process across requests or jobs. Static properties, container singletons holding request data, and in-memory caches leak between requests, and memory grows. Reset request state explicitly, and restart workers periodically (`php artisan queue:work --max-jobs=1000 --max-time=3600`, `messenger:consume --limit=1000 --memory-limit=256M`). - Fibers (8.1) are the primitive under AMPHP v3, ReactPHP and Revolt; application code rarely touches them directly. - Parallel or deferred work goes through a queue (Laravel queues, Symfony Messenger) or separate processes (`symfony/process`). ## Laravel Essentials - **N+1:** eager load with `with('customer')`; call `Model::preventLazyLoading(! app()->isProduction())` (or `Model::shouldBeStrict()`, which also rejects silently discarded and missing attributes) in `AppServiceProvider::boot`. - **Mass assignment:** `$fillable` is the allowlist. Pass `$request->validated()` from a FormRequest; never `$guarded = []` together with `$request->all()`. - **Configuration:** call `env()` only inside `config/*.php`. After `php artisan config:cache`, `env()` elsewhere returns `null`; read `config('services.stripe.key')`. - **Transactions and queues:** `DB::transaction(fn () => ...)`; dispatch jobs from inside it with `->afterCommit()` so a worker never sees uncommitted rows. `SerializesModels` re-fetches models by key when the job runs; handlers must be idempotent (`$tries`, `backoff()`, `ShouldBeUnique`). - **Large tables:** `chunkById` / `lazyById`; `chunk` while updating the filtered column skips rows. - **Laravel 11+** configures middleware, exceptions and routing in `bootstrap/app.php`. - **Tests:** `RefreshDatabase`, `Http::fake()`, `Queue::fake()`, `Mail::fake()`, `$this->actingAs($user)`. ```php public function store(StoreOrderRequest $request): JsonResponse { $order = DB::transaction(function () use ($request): Order { $order = Order::create($request->safe()->except('lines')); $order->lines()->createMany($request->validated('lines')); SendOrderConfirmation::dispatch($order)->afterCommit(); return $order; }); return OrderResource::make($order->load('lines'))->response()->setStatusCode(201); } ``` ## Symfony Essentials - Services are autowired and autoconfigured by default (`config/services.yaml`); inject through constructors. Tag with attributes: `#[AsEventListener]`, `#[AsMessageHandler]`, `#[AsCommand]`. - Routes via `#[Route]`; `#[MapRequestPayload]` (6.3+) deserializes and validates a request DTO. - Doctrine: `persist()` schedules, `flush()` writes every pending change in one transaction. Flush once per unit of work; for imports, flush and `clear()` in batches or memory grows with the identity map. Lazy associations cause N+1; fetch-join in the query (`->addSelect('l')`). - Authorization through voters (`#[IsGranted('EDIT', subject: 'order')]`); forms carry CSRF tokens by default. - Diagnose with `bin/console debug:container`, `debug:router`, `lint:container`. ```php #[Route('/api/orders', methods: ['POST'])] public function create(#[MapRequestPayload] CreateOrder $command, MessageBusInterface $bus): JsonResponse { $bus->dispatch($command); return new JsonResponse(null, Response::HTTP_ACCEPTED); } ``` ## Testing **PHPUnit (10+).** ```php <?php declare(strict_types=1); namespace Tests\Unit\Spool; use App\Spool\SymlinkException; use App\Spool\Writer; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; final class WriterTest extends TestCase { private string $root; protected function setUp(): void { $this->root = sys_get_temp_dir() . '/spool-' . bin2hex(random_bytes(4)); mkdir($this->root); } public function testRejectsSymlinkedSpoolDirectory(): void { mkdir($this->root . '/target'); symlink($this->root . '/target', $this->root . '/spool'); $this->expectException(SymlinkException::class); (new Writer($this->root . '/spool'))->write('job-1', ['payload']); } #[DataProvider('invalidJobIds')] public function testRejectsInvalidJobId(string $jobId): void { $this->expectException(\InvalidArgumentException::class);
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub