Skip to main content

loom-php

PHP language expertise for idiomatic, production-quality code.

소스 정보

저장소
cosmix/loom
최근 소스 활동
2026년 9월 25일 06:00
감지된 SKILL.md 언어
영어
스타
61
포크
4

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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);
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기