Skip to content

10. Layered Applications

10.1. Introduction

The scripts for this chapter are in the [scripts-console/three-layers] folder:

1
2
3
4
5
6
7
8
9
php8-oct-2026/scripts-console/three-layers
├── Business.php
├── Dao1.php
├── Dao2.php
├── IBusiness.php
├── IDao.php
├── main1.php
├── main2.php
└── Person.php

We will now explore how to structure a PHP application in layers:

Image

In this diagram, the left layer initiates the use of the right layer. The roles of the layers are as follows:

  • [1]: The layer called [dao] (Data Access Objects) handles interactions with external data stores [4] (files, databases, web services, etc.). This layer is sometimes referred to as [DAL] (Data Access Layer), a term that better describes its role. This layer can read data from [3] or write data to [2]. It is called by the [business] [6] layer and returns results [7] to it;
  • [5]: a layer called [business] that brings together “business” procedures and is supplied with all the data they need. This is generally the most stable layer in a project because it does not depend on how the data is acquired. The data comes from two sources:
    • [9]: data provided by the PHP script;
    • [6, 7]: data requested from the [dao] layer;
  • [8]: the main script acts as the orchestrator. In a console application, it will:
    • create the layers [business] and [dao];
    • execute the application’s algorithm. This algorithm functions like an orchestrator: it contains no “business logic” or data access code. The main script simply calls the procedures of the [business] layer [9]. It completely ignores the [dao] layer and external data. It can provide the [business] layer with [9] data. In a console application, this data may come from configuration files or from the script user. It receives [10] results from the [business] layer. It may need to store certain results: to do so, it again uses the procedures of the [business] layer, which in turn will call upon the [dao] layer to perform the task;
    • Among the results, the main script may receive exceptions. It is the main script’s role to handle exceptions that are propagated up from all layers.

This layered architecture is designed to facilitate the application’s evolution. To achieve this, each layer implements an interface. Suppose that:

  • the [dao] layer is implemented by a class [Dao], which implements an interface [IDao];
  • the [business] layer is implemented by a class [Business], which implements an interface [IBusiness].

The procedures in layer [business] will then use interface [IDao] instead of class [Dao]. This allows the [dao] layer to be updated without affecting the [business] layer. Suppose that in version 1, the [Dao1] layer uses data from a database. Following an update, this data is now provided by a web service in version 2, [Dao2]. We will ensure that both classes, [Dao1] and [Dao2], implement the same interface, [IDao], and that they throw the same exceptions. If this is verified, the [Business] layer, which works with the [IDao] interface, will remain unchanged.

The same reasoning applies to the [business] layer.

The application will implement the following layered structure:

Image

10.2. Objects Exchanged Between Layers

Normally, layers exchange various objects. Here, they will exchange objects of the following [Person] class: [Person.php]:

<?php

declare(strict_types=1);

// a person: an immutable object
final readonly class Person
{
    // constructor with promotion of the [id] attribute
    public function __construct(public int $id)
    {
    }

    // toString
    public function __toString(): string
    {
        return "[Person($this->id)]";
    }
}

Comments

  • line 6: Objects exchanged between layers contain only data. They are made into immutable objects (class [readonly]): this prevents a layer from inadvertently modifying an object it received from another layer;
  • line 9: The person has only one attribute, their identifier [$id], initialized by the constructor through parameter promotion. We will assume that this identifier refers to a unique person in a data warehouse;
  • Lines 14–17: The method [__toString], which displays the person’s identifier.

10.3. [dao] Layer

The [IDao] interface implemented by the [dao] layer is as follows: [IDao.php]:

<?php

declare(strict_types=1);

// [dao] layer ------------------------
interface IDao
{
    // retrieving a person from an external repository
    // the identifier of the person is passed
    public function get(int $id): Person;

    // saving a person in an external repository
    public function save(Person $p): void;
}

This interface is implemented by the following [Dao1] class: [Dao1.php]:

<?php

declare(strict_types=1);

final class Dao1 implements IDao
{
    // saving a person in an external repository
    #[\Override]
    public function save(Person $p): void
    {
        print "[Dao1]: Saving the person $p in the [local] database\n";
    }

    // retrieving a person from an external repository
    #[\Override]
    public function get(int $id): Person
    {
        print "[Dao1]: Retrieving the person with identity ($id) from the [local] database\n";
        return new Person($id);
    }
}

Comments

  • Line 5: The class [Dao1] implements the interface [IDao]. It is declared as [final]: it is not intended to be derived from;
  • the class does not interact with a data warehouse. It simply displays messages to track the code’s execution (line 11, line 18);
  • line 8: the attribute [#[\Override]] indicates that the method implements a method of the interface. If the method name is incorrect, PHP signals this;
  • line 19: We do indeed return a person with the ID passed as a parameter to the method.

We also implement the [IDao] interface with the following [Dao2] class, [Dao2.php]:

<?php

declare(strict_types=1);

final class Dao2 implements IDao
{
    // saving a person in an external repository
    #[\Override]
    public function save(Person $p): void
    {
        print "[Dao2]: Saving the person $p in the [remote] database\n";
    }

    // retrieving a person from an external repository
    #[\Override]
    public function get(int $id): Person
    {
        print "[Dao2]: Retrieving the person with identity ($id) from the [remote] database\n";
        return new Person($id);
    }
}

The [Dao2] class is similar to the [Dao1] class, except that we have modified the messages displayed.

10.4. Layer [business]

Layer [business] provides the following [IBusiness] interface: [IBusiness.php]:

<?php

declare(strict_types=1);

// [business] layer ------------------------
interface IBusiness extends IDao
{
    // we process a person
    public function doSomething(Person $p): void;
}

Comments

  • Line 6: The [IBusiness] interface extends the [IDao] interface. This is not required at all. We’re doing this here because the example is simple: the [business] layer provides the main script with the [get] and [save] methods from the [dao] layer, which it hides from the main script;
  • Line 9: The method [doSomething] is specific to the [business] layer.

The PHP 7 course referred to the interface [IMétier] and the class [Métier], with an accent. PHP allows accented letters in identifiers, but a file named [Métier.php] causes problems with certain tools (ZIP archives, file systems, automatic class loading). We now stick to class and file names without accents: here, [IBusiness] and [Business].

The [IBusiness] interface is implemented by the following class: [Business] [Business.php]:

<?php

declare(strict_types=1);

final class Business implements IBusiness
{
    // the [dao] layer is injected by the constructor
    public function __construct(private readonly IDao $dao)
    {
    }

    // we process a person
    #[\Override]
    public function doSomething(Person $p): void
    {
        // processing
        print "[Business]: Business processing of the person $p\n";
    }

    // saving the person
    #[\Override]
    public function save(Person $p): void
    {
        print "[Business]: Saving the person $p\n";
        // we ask the [dao] layer to do the saving
        $this->dao->save($p);
    }

    // retrieving a person
    #[\Override]
    public function get(int $id): Person
    {
        print "[Business]: Retrieving the person with identifier $id\n";
        // we ask the [dao] layer for the person
        return $this->dao->get($id);
    }
}

Comments

  • Line 8: The [business] layer must have a reference to the [dao] layer in order to use the latter’s methods. This reference is passed to it via its constructor and stored in the private, read-only attribute [$dao] (parameter promotion). Note that the parameter type is the [IDao] interface. Thus, the [business] layer will be able to work with any class that implements the [IDao] interface. If we switch from a [Dao1] layer to a [Dao2] layer, and both implement the [IDao] interface, the [business] layer does not need to be rewritten;
  • this technique is called constructor-based dependency injection: the class [Business] does not create the layer [dao] on which it depends; instead, it is provided to it. The PHP 7 course used a setter [setDao]. Constructor injection is now the norm: a [Business] object cannot exist without its [dao] layer, and this layer cannot be changed afterward ([readonly] attribute). This is also the approach taken by frameworks such as Symfony or Laravel;
  • lines 14–18: implementation of [IBusiness::doSomething];
  • lines 22–27: implementation of [IBusiness::save]. This method must save a person to a data warehouse. The [business] layer cannot do this. It calls upon the [dao] layer to perform this save (line 26);
  • lines 31–36: implementation of [IBusiness::get]. This method must retrieve from a data warehouse the person whose ID is passed to it as a parameter. The [business] layer cannot do this. It calls on the [dao] layer to perform the task (line 35).

Conclusion: Whenever the [business] layer needs to access data stored in the data warehouse, it must go through the [dao] layer, which was created specifically to access that data.

10.5. Main Script

We will write two orchestration scripts for this application. The first, [main1.php], will use the [Dao1] layer, while the second, [main2.php], will use the [Dao2] layer. We want to show that this has no impact on the code in the [Business] layer.

The [main1.php] script is as follows:

<?php

// strict respect of the declared types of function parameters and results
declare(strict_types=1);

// inclusion of the classes and interfaces
require_once __DIR__ . "/Person.php";
require_once __DIR__ . "/IDao.php";
require_once __DIR__ . "/IBusiness.php";
require_once __DIR__ . "/Dao1.php";
require_once __DIR__ . "/Business.php";

// creation of the layers: the [dao] layer is injected into the [business] layer
$business = new Business(new Dao1());
// use of the [business] layer
$person = $business->get(4);
$business->doSomething($person);
$business->save($person);

Comments

  • The [main1.php] script acts as the application's orchestrator. It loads the class and interface files (lines 7–11), creates the application’s layered structure (line 14), and then communicates with the [business] layer (lines 16–18);
  • line 14: it is here, and only here, that the implementation of the [dao] layer is selected;
  • According to the layered structure diagram, the [main1.php] script must communicate only with the [business] layer. It must not communicate with the [dao] layer, even though this is theoretically possible.

The results of the execution are as follows:

1
2
3
4
5
6
> php main1.php
[Business]: Retrieving the person with identifier 4
[Dao1]: Retrieving the person with identity (4) from the [local] database
[Business]: Business processing of the person [Person(4)]
[Business]: Saving the person [Person(4)]
[Dao1]: Saving the person [Person(4)] in the [local] database

Comments

  • Line 16 of the code caused lines 2 and 3 of the results to be written;
  • Line 17 of the code caused line 4 of the results to be written;
  • Line 18 of the code caused lines 5 and 6 of the results to be written.

The script [main2.php] is as follows:

<?php

// strict respect of the declared types of function parameters and results
declare(strict_types=1);

// inclusion of the classes and interfaces
require_once __DIR__ . "/Person.php";
require_once __DIR__ . "/IDao.php";
require_once __DIR__ . "/IBusiness.php";
require_once __DIR__ . "/Dao2.php";
require_once __DIR__ . "/Business.php";

// creation of the layers: the [dao] layer is injected into the [business] layer
$business = new Business(new Dao2());
// use of the [business] layer
$person = $business->get(4);
$business->doSomething($person);
$business->save($person);

Comments

  • Lines 10 and 14: The layer structure now uses the [Dao2] layer. These are the only differences from [main1.php].

The results of the execution are as follows:

1
2
3
4
5
6
> php main2.php
[Business]: Retrieving the person with identifier 4
[Dao2]: Retrieving the person with identity (4) from the [remote] database
[Business]: Business processing of the person [Person(4)]
[Business]: Saving the person [Person(4)]
[Dao2]: Saving the person [Person(4)] in the [remote] database

The [Dao1] layer simulates access to a local database, while the [Dao2] layer simulates access to a remote database. As long as these two layers conform to the [IDao] interface, we can see that the code for the [Business] layer did not need to be changed.

We’ll apply what we’ve just learned to the tax calculation exercise that serves as our guiding example.