Page tree

Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Expand
title3. Application conceptions

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:

  1. 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.
  2. Reusability:

    • Operations can be reused independently across different parts of the application, promoting consistency and reducing duplication.
  3. Testability:

    • Single-purpose classes are easier to unit test, as they have well-defined inputs and outputs.
    • Mocking dependencies for individual operations is straightforward.
  4. 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.
  5. Decoupling:

    • Operations remain loosely coupled to each other, making them easier to refactor or replace.
  6. 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.
  7. 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.


Code Block
languagephp
titleExample
collapsetrue
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 = \ZfExtended_Factory::get(TaskAssociation::classnew \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).


...