Skip to main content

coding-conventions

Coding principles and conventions for this Shopsys project — DRY/KISS/reuse, comment and docblock quality, test-code rules, the project visibility/typing rules, and documentation best practices. Read it before writing or modifying PHP so your code matches the codebase.

Source facts

Repository
shopsys/project-base
Last source activity
November 2, 2026 at 07:27
Detected SKILL.md language
English
Stars
38
Forks
12

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
coding-conventions
description
Coding principles and conventions for this Shopsys project — DRY/KISS/reuse, comment and docblock quality, test-code rules, the project visibility/typing rules, and documentation best practices. Read it before writing or modifying PHP so your code matches the codebase.
> **Monorepo note:** if the repository has top-level `packages/` and `project-base/` directories (the shopsys/shopsys monorepo layout, not a standalone project), also read `.agents/skills/monorepo-vs-project/SKILL.md` and apply its delta (package-first + path/role remap) on top of this skill. # Shopsys coding conventions Follow these whenever you write or modify code in this project. ## Core principles ### DRY (Don't Repeat Yourself) - **First**, check whether existing functionality can be reused — utilities, helpers, methods, established patterns (in `app/src/` and in `vendor/shopsys/`). - Extend or compose existing solutions rather than duplicating logic; extract common patterns into reusable components. ### KISS (Keep It Simple) - Choose the simplest solution that works; avoid over-engineering and clever abstractions. - Ask: "Is there a simpler way using existing code?" ### Reuse & composition - Leverage tested, proven functionality; build complex behavior from simple existing blocks. - Use framework conventions; prefer composition over inheritance or duplication. ### Validation & guard clauses - Prefer the `Webmozart\Assert\Assert` package over hand-written `if (...) { throw new ... }` guards for argument and state validation. It is already a dependency, keeps the guard on one line, and makes the intent obvious (`Assert::notSame()`, `Assert::isEmpty()`, `Assert::allInstanceOf()`, `Assert::keyExists()`, …). - Narrow `object` or interface-typed parameters with `Assert::isInstanceOf($entity, Order::class)` instead of a `/** @var */` docblock or a manual `instanceof` check. PHPStan understands the assert (`phpstan/phpstan-webmozart-assert` is enabled in `phpstan.neon`), so the following code is analysed with the concrete type, and a wrong instance fails fast at runtime with a clear message. - Pass the message as a closure when building it is not trivial — `Assert::isEmpty($keys, fn (): string => sprintf(...))` — so it is only built when the assertion actually fails. - The message is passed through `sprintf()` by the library, so avoid a literal `%` in it. - Write a dedicated exception class instead when callers need to catch that specific failure — `Assert` always throws `\InvalidArgumentException`. ### Maintainability first - Write code that's easy to understand, modify, and debug; change things in the fewest places. - Follow existing conventions; favor long-term maintainability over short-term convenience. ### Before writing new code, ask: - Does this already exist in the codebase (`app/src/`, `vendor/shopsys/`)? Can I combine existing functions/methods? - What's the simplest approach using what's available? Will it be easy to maintain? - Does each comment add value? Do my arrays/callables/complex types have proper docblocks? ## Comments — quality over quantity - **Only** add comments that provide real value. Write self-documenting code with clear names. - Explain **why**, not **what**; focus on business logic, complex algorithms, non-obvious decisions. ```php // GOOD // Handle edge case where role doesn't have FULL permission available // Workaround for legacy database structure - remove after migration to v2.0 // Algorithm based on RFC 3986 specification // BAD // This method returns the name public function getName(): string // Set the user ID $user->setId($id); ``` ## DocBlocks — always specify non-obvious types Add docblocks for non-obvious parameters and return types (helps IDEs and static analysis). Always specify: `array` contents, `class-string` targets, `callable` signatures, `iterable` element types, complex/generic return types. ```php // GOOD /** * @param class-string<\Shopsys\FrameworkBundle\Component\Context\AbstractContext> $context * @param array<string> $excludedRoles * @param array<int, \Shopsys\FrameworkBundle\Component\Security\Role\Role> $roles * @return array<string, mixed> */ public function buildGrid(string $context, array $excludedRoles, array $roles): array /** * @param callable(string): bool $validator * @param iterable<\App\Entity\User> $users * @return \Generator<int, string> */ public function processUsers(callable $validator, iterable $users): \Generator // BAD — what's in the array? which class? array of what? /** * @param array $excludedRoles * @param string $context * @return array */ public function buildGrid(string $context, array $excludedRoles): array ``` ### `{@inheritdoc}` vs a detailed docblock - Use `{@inheritdoc}` when the interface docblock is comprehensive, no extra type specificity is needed, and there's no implementation-specific behavior to document. - Write a detailed docblock when you can add specific array/generic types, concrete examples, or document implementation-specific behavior (e.g. specifying a `mixed` the interface leaves generic). ```php // Simple case → {@inheritdoc} /** * {@inheritdoc} */ public function findById(int $id): ?User { /* ... */ } // Adds value → specific docblock /** * Transforms role identifiers array to multidimensional form structure * * @param array<string>|mixed $value Array of role identifiers (e.g., ['ROLE_ORDER_VIEW']) * @return array<string, array<string, bool>> [roleConstant][permission] = bool */ public function transform(mixed $value): array { /* ... */ } ``` ## Test-code rules - In functional and GraphQL test cases, inject services with the `@inject` annotation — **not** `$this->getContainer()->get(...)`. - Use `.agents/skills/test-writing/SKILL.md` to select the domain guide: `backend-tests` for PHP and admin Jest, `storefront-tests` for Vitest and Cypress routing. ## Entity property hooks Sanitization, single-property validation and null fallbacks of an entity property live in a `set` property hook, never only in a setter (a constructor, `edit()` or a subclass can bypass a setter). Rules and examples: `docs/model/entities.md#property-hooks`. ## Visibility & typing Your project code is a final version used as-is, with no downstream extension surface to preserve: - use `final` on classes; - use `private` visibility (not `protected`); - use typehints and return types everywhere. The framework you extend lives read-only in `vendor/shopsys/` and follows different rules there (it's built to be extended) — but that's its code, not yours. For how to extend it correctly, see `.agents/skills/shopsys-architecture/SKILL.md`.
View on GitHub