- Created by Ines-Paul Baumann, last modified by Stephan Bergmann on Nov 22, 2024
Every developer should follow the PHP Standards Recommendations
1.1 Variables:
- Use camelCase for variable names.
- Variable names should be descriptive and concise.
- Avoid abbreviations unless widely recognized.
$userName = 'John'; $totalAmount = 100;
1.2 Functions and Methods:
- Use camelCase for function and method names.
- Function and method names should clearly express their purpose.
- Methods should start with a verb when possible.
function calculateTotalAmount($prices) { // Logic here }
1.3 Classes:
- Use PascalCase for class names.
- Class names should be nouns and describe the entity or the functionality.
class UserController { // Class definition }
1.4 Constants:
- Use UPPER_CASE with underscores (screaming snake case) for constants.
- Constants should be descriptive and represent fixed values.
define('MAX_RETRY_ATTEMPTS', 5); const DEFAULT_TIMEOUT = 30;
1.5 Interfaces:
- Use PascalCase with the suffix Interface.
- Interfaces should describe behavior and represent contracts.
interface CacheInterface { public function get(string $key): int; public function set(string $key, string $value); }
1.6 Abstract classes
- Use PascalCase with the prefix Abstract.
abstract class AbstractJob { abstract public function getName(): string; // Other logic here }
1.7 Namespaces:
- Use PascalCase for namespaces.
- Namespace names should map to the directory structure.
namespace App\Controllers;
2.1 Dependency Injection (DI):
- All class dependencies must be explicit and provided through Dependency Injection.
- Avoid creating dependencies within the class or using global state (e.g.,
global
variables orsingleton
patterns). - Use constructor injection or method injection for better testability and maintainability.
class CreateTaskTmOperation { public function __construct( private readonly TaskRepository $taskRepository, private readonly LanguageResourceRepository $languageResourceRepository, private readonly CreateLanguagePairOperation $createLanguagePairOperation, private readonly CustomerAssocService $customerAssocService, private readonly ServiceManager $serviceManager, private readonly TaskTmTaskAssociationRepository $taskTmTaskAssociationRepository, ) { } }
2.2 New Code Placement:
- All new code must be placed in the
/src
folder. - Code must adhere to PSR standards, including:
- Proper use of namespaces to reflect the directory structure.
- Avoidance of PHP 4-style class names (no underscores for class name simulation).
2.3 SOLID Principles:
- All new code must follow the SOLID principles:
- Single Responsibility Principle: A class should have one and only one reason to change.
- Open/Closed Principle: Code should be open for extension but closed for modification.
- Liskov Substitution Principle: Subtypes must be substitutable for their base types.
- Interface Segregation Principle: Classes should not be forced to implement unnecessary interfaces.
- Dependency Inversion Principle: Depend on abstractions, not on concrete implementations.
2.4 Unit Tests:
- All new classes must have corresponding unit tests.
- Tests should cover the main functionality and edge cases.
- Follow the AAA structure for tests (Arrange, Act, Assert).
use PHPUnit\Framework\TestCase; class PaymentServiceTest extends TestCase { public function testProcessPayment() { // Arrange $mockGateway = $this->createMock(PaymentGatewayInterface::class); $service = new PaymentService($mockGateway); // Act $result = $service->processPayment(100); // Assert $this->assertTrue($result); } }
2.5 Encapsulation Over Inheritance:
- Prefer encapsulation over extending classes.
- Use inheritance only when a clear “is-a” relationship exists.
Problem with Inheritance:
Using inheritance might seem natural when trying to extend functionality, but it can lead to tightly coupled designs and rigid hierarchies.
class FileStorage { public function save(string $fileName, string $data): void { // Save the file to disk echo "Saving to disk: $fileName\n"; } } class LoggingFileStorage extends FileStorage { public function save(string $fileName, string $data): void { // Log the action before saving echo "Logging: Saving $fileName\n"; parent::save($fileName, $data); } }
Issues:
- The
LoggingFileStorage
class is tightly coupled to theFileStorage
implementation. - If the base class changes,
LoggingFileStorage
might break or require updates. - Adding more variations (e.g., caching, encrypting) will create a complex and rigid inheritance hierarchy.
Better with Encapsulation:
Encapsulation allows us to compose functionality dynamically and avoid the pitfalls of deep inheritance.
interface StorageInterface { public function save(string $fileName, string $data): void; } class FileStorage implements StorageInterface { public function save(string $fileName, string $data): void { // Save the file to disk echo "Saving to disk: $fileName\n"; } } class LoggingStorage implements StorageInterface { public function __construct(private readonly StorageInterface $storage) { $this->storage = $storage; } public function save(string $fileName, string $data): void { // Log the action before saving echo "Logging: Saving $fileName\n"; $this->storage->save($fileName, $data); } } class CachingStorage implements StorageInterface { public function __construct(private readonly StorageInterface $storage) { $this->storage = $storage; } public function save(string $fileName, string $data): void { // Save to cache before saving to storage echo "Caching: $fileName\n"; $this->storage->save($fileName, $data); } }
Advantages of Encapsulation Over Inheritance:
- Flexibility: Functionality can be composed dynamically without changing the underlying classes.
- Reusability: Decorators can be reused across different storage implementations (e.g., database, cloud storage).
- Scalability: Adding new behavior (e.g., encryption) doesn’t require modifying existing classes or creating a rigid hierarchy.
This approach adheres to the Open/Closed Principle (code is open for extension but closed for modification) and results in more maintainable, testable, and decoupled code.
2.6 Interface Usage:
- Always program to interfaces, not implementations.
- Use interfaces to define contracts and ensure flexibility for future changes.
2.7 Small, Focused Methods:
- Keep methods small and focused on a single task.
- If a method grows beyond 15–20 lines, consider refactoring into smaller, reusable methods.
2.8 Avoid Hardcoding:
- Avoid hardcoding values or logic directly into your classes.
- Use configuration files, environment variables, or constants for flexibility.
class RetryHandler { private const MAX_RETRY_COUNT = 5; public function handle() { for ($i = 0; $i < self::MAX_RETRY_COUNT; $i++) { // Retry logic } } }
2.9 Readable Code:
- Prioritize readability over clever or overly complex code.
- Use meaningful variable and method names, and write inline comments for non-obvious logic. Important here is not to write comments that describe what the code is doing because it is obvious from the code itself, but to write comments describing the logic behind, WHY it is doing this.
2.10 Avoid Side Effects:
- Functions and methods should avoid unexpected side effects.
- For example, avoid modifying global state or altering input parameters.
- Functions that are designed to do some checks should not mutate or create something and vice versa
3.1 Avoid using framework base classes and services directly.
Generally writing framework-agnostic code ensures that application logic remains decoupled from the underlying framework, making codebase more flexible, portable, and maintainable. It allows to switch frameworks or update to newer versions with minimal effort, as the core business logic is not tightly bound to a framework features or structures. This practice promotes reusability. Additionally, it facilitates better unit testing, as the code can be tested in isolation without relying on the framework’s environment or tools.
And for sure Zend 1 is a very outdated framework and we definitely need to migrate to something newer.
- Do not use ZfExtended_Factory except for models. Instead use $xyz = new NeededClass();
- Do not use Zend_Http_Client, use Guzzle and rely on PSR HTTP message interfaces
TBA
3.2 Operations (Use cases)
The Operations approach focuses on creating small, single-purpose classes, each dedicated to performing a specific task within the application. Instead of bundling multiple related actions (e.g., create, update, delete) into a single service class, each action is encapsulated in its own class, such as CreateOperation
, UpdateOperation
, or AssignRolesOperation
.
This approach, often referred to as the Use Cases pattern in some repositories, aligns with the principle of Single Responsibility and ensures that each class has one clearly defined purpose.
Advantages:
Clarity and Maintainability:
- Each operation class is small and easy to understand, reducing cognitive load for developers.
- Changes to one operation do not risk unintended side effects on other functionalities.
Reusability:
- Operations can be reused independently across different parts of the application, promoting consistency and reducing duplication.
Testability:
- Single-purpose classes are easier to unit test, as they have well-defined inputs and outputs.
- Mocking dependencies for individual operations is straightforward.
Scalability:
- Adding new operations does not bloat existing classes. For example, introducing a new user-related action only requires creating a new
NewOperation
class without modifying existing ones.
- Adding new operations does not bloat existing classes. For example, introducing a new user-related action only requires creating a new
Decoupling:
- Operations remain loosely coupled to each other, making them easier to refactor or replace.
Explicit Workflow Representation:
- By naming each class according to its specific purpose (e.g.,
CreateOperation
,AssignRolesOperation
), the application’s workflow becomes more explicit and self-documenting.
- By naming each class according to its specific purpose (e.g.,
Alignment with Domain-Driven Design:
- The approach ties each operation to a single use case or action in the domain, making the codebase more reflective of business logic.
class AssociateTaskOperation { public function __construct( private readonly LanguageResourceTaskAssocRepository $taskAssocRepository ) { } /** * @codeCoverageIgnore */ public static function create(): self { return new self( new LanguageResourceTaskAssocRepository() ); } public function associate( int $languageResourceId, string $taskGuid, bool $segmentsUpdatable = false, bool $autoCreateOnImport = false ): TaskAssociation { $taskAssoc = new \TaskAssociation(); $taskAssoc->setLanguageResourceId($languageResourceId); $taskAssoc->setTaskGuid($taskGuid); $taskAssoc->setSegmentsUpdateable($segmentsUpdatable); $taskAssoc->setAutoCreatedOnImport((int) $autoCreateOnImport); $this->taskAssocRepository->save($taskAssoc); return $taskAssoc; } }
Operation classes should be placed in a folder of a feature where they are applicable e.g. operations that are responsible for working with user in src/User/Operation, operations for language resource in src/LanguageResource/Operation, however more specific operation should be placed in feature folder e.g. src/LanguageResource/TaskTm/Operation
3.3 Repository Classes for Data Access
In our application, the current use of Active Record ORM involves placing data access and persistence logic directly within model classes. To improve separation of concerns and maintainability, we should adopt the repository pattern. Repository classes act as a dedicated layer for handling all database interactions, encapsulating queries and persistence logic. This approach ensures that model classes focus solely on representing business entities, while repositories centralize data access, making it easier to manage, test, and reuse. For instance, instead of embedding custom query methods within the User
model, a UserRepository
can handle operations like findByEmail
, findActiveUsers
, or save
. This transition not only aligns with best practices but also prepares our codebase for future scalability and flexibility, such as swapping the ORM or database without affecting business logic.
Repositories should be placed in src/Repository folder. But there might be a specific repository methods needed in particular plugin code or in particular features - then it is better to create a separate repo and place it to the specific place (feature of plugin).
Every developer should follow the following principles:
- YAGNI: You ain’t gonna need it (wiki)
- KISS: Keep It Simple, Stupid (wiki)
- DRY: Don’t Repeat Yourself (wiki)
- No labels
1 Comment
Stephan Bergmann
Sadly this can not be added as inline-comment, cause its inside a Code-block.