Неймспейсы и автозагрузка в PHP

В этом уроке мы коснемся архитектуры приложений. Если быть точнее – мы научимся тому, как в современном программировании на PHP принято хранить классы в отдельных файлах, и о том, как избегать при этом бесконечных строчек с include и require для подключения этих файлов.

На самом деле, в PHP всё довольно просто с правилами по реализации большинства частей приложения. Для этого есть уже придуманные умными людьми стандарты – PSR (PHP Standards Recommendations). В них описано, как нужно писать ту или иную составляющую вашей программы.

В этом уроке мы затронем стандарт PSR-4. В нём говорится о том, что каждый класс должен храниться в отдельном файле и находиться в пространстве имён. Давайте обо всём по порядку.

Пусть у нас есть классы User и Article. Нам нужно сохранить их в разных файлах. Для этого давайте создадим рядом с папкой www папку src, а внутри неё папку MyProject. Внутри папки MyProject создадим папку Models, а в ней создадим ещё 2 папки – Articles и Users. И уже в этих папках создадим файлы Article.php и User.php. Должно получиться следующее:

Архитектура проекта

Давайте теперь опишем в этих двух файлах наши классы.
src/MyProject/Models/Articles/Article.php

<?php

class Article
{
    private $title;
    private $text;
    private $author;

    public function __construct(string $title, string $text, User $author)
    {
        $this->title = $title;
        $this->text = $text;
        $this->author = $author;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getText(): string
    {
        return $this->text;
    }

    public function getAuthor(): User
    {
        return $this->author;
    }
}

src/MyProject/Models/Users/User.php

<?php

class User
{
    private $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

Первую часть сделали – теперь у нас каждый класс лежит в отдельном файле. Давайте теперь перейдём в наш index.php, лежащий в директории www и запишем в него логику для работы с этими классами.

<?php

$author = new User('Иван');
$article = new Article('Заголовок', 'Текст', $author);
var_dump($article);

Давайте теперь попробуем запустить этот скрипт в браузере.
Разумеется, мы получим ошибку.

Ошибка из-за ненайденного класса

Нашему скрипту не удалось найти класс User. Давайте подключим файлы с нужными нам классами в начале index.php

<?php

require __DIR__ . '/../src/MyProject/Models/Users/User.php';
require __DIR__ . '/../src/MyProject/Models/Articles/Article.php';

$author = new User('Иван');
$article = new Article('Заголовок', 'Текст', $author);
var_dump($article);

Если мы сейчас запустим этот скрипт, то всё у нас прекрасно отработает и мы увидим результат var_dump().

Результат успешной работы скрипта

Итак, с первым пунктом про хранение классов в отдельных файлах мы разобрались.

Пространства имён - namespaces

Теперь вернёмся к пространствам имён – неймспейсам. Тут всё довольно просто – класс можно поместить в отдельное именованное пространство и в дальнейшем использовать его по этому полному имени. Для того чтобы указать это пространство для конкретного класса используется слово namespace, за которым следует само имя. Указывается оно в файле с классом, перед определением класса. На примере класса User это будет выглядеть следующим образом:
src/MyProject/Models/Users/User.php

<?php

namespace MyProject\Models\Users;

class User
{
    private $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

Теперь мы можем говорить, что класс User находится в неймспейсе MyProject\Models\Users.

Давайте проделаем аналогичные действия с классом Article.
src/MyProject/Models/Articles/Article.php

<?php

namespace MyProject\Models\Articles;

class Article
{
    private $title;
    private $text;
    private $author;

    public function __construct(string $title, string $text, User $author)
    {
        $this->title = $title;
        $this->text = $text;
        $this->author = $author;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getText(): string
    {
        return $this->text;
    }

    public function getAuthor(): User
    {
        return $this->author;
    }
}

Теперь, чтобы в файле index.php работать с данными классами, мы должны указать полное имя класса – это имя класса с указанием его неймспейса. Делается это следующим образом.
www/index.php

<?php

require __DIR__ . '/../src/MyProject/Models/Users/User.php';
require __DIR__ . '/../src/MyProject/Models/Articles/Article.php';

$author = new \MyProject\Models\Users\User('Иван');
$article = new \MyProject\Models\Articles\Article('Заголовок', 'Текст', $author);
var_dump($article);

Если мы сейчас запустим скрипт, то снова столкнёмся с ошибкой.

Ошибка из-за разных неймспейсов

Но на этот раз, она уже другая. А именно – третий аргумент, переданный в конструктор класса Article должен быть объектом класса MyProject\Models\Articles\User, а передан объект класса MyProject\Models\Users\User. Заметили ошибку? Неймспейс не тот. Дело в том, что если в файле с классом указан неймспейс, то все классы, которые указываются в данном файле будут искаться в том же неймспейсе. Так как у нас класс User находится в другом неймспейсе, то мы должны явно это указать. Вот так:
src/MyProject/Models/Articles/Article.php

<?php

namespace MyProject\Models\Articles;

class Article
{
    private $title;
    private $text;
    private $author;

    public function __construct(string $title, string $text, \MyProject\Models\Users\User $author)
    {
        $this->title = $title;
        $this->text = $text;
        $this->author = $author;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getText(): string
    {
        return $this->text;
    }

    public function getAuthor(): \MyProject\Models\Users\User
    {
        return $this->author;
    }
}

Либо же указать в начале файла о каком классе идёт речь, когда мы используем в коде только слово User. Делается это с помощью слова use после указания текущего неймспейса, но перед описанием класса.

<?php

namespace MyProject\Models\Articles;

use MyProject\Models\Users\User;

class Article
{
    private $title;
    private $text;
    private $author;

    public function __construct(string $title, string $text, User $author)
    {
        $this->title = $title;
        $this->text = $text;
        $this->author = $author;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getText(): string
    {
        return $this->text;
    }

    public function getAuthor(): User
    {
        return $this->author;
    }
}

Теперь, когда мы будем использовать класс User, то автоматически будет использоваться класс из неймспейса MyProject\Models\Users\User.

Давайте снова запустим скрипт, и убедимся, что всё у нас теперь работает.

object(MyProject\Models\Articles\Article)[2]
  private 'title' => string 'Заголовок' (length=18)
  private 'text' => string 'Текст' (length=10)
  private 'author' => 
    object(MyProject\Models\Users\User)[1]
      private 'name' => string 'Иван' (length=8)

Получили работающий скрипт. Ура! Вот в принципе и всё, что вам нужно знать о неймспейсах. В наших будущих программах мы всегда будем создавать классы внутри неймспейсов.

Автозагрузка

Однако, давайте снова посмотрим на наш файл index.php. представьте, что у нас теперь большой проект и в нём больше 100 классов. Нам придётся сто раз писать require с указанием каждого файла. Утомительно, да? Однако, можно автоматизировать этот процесс, написав функцию автозагрузки классов. Она будет вызываться каждый раз, когда впервые будет встречаться новый класс.

Вы заметили, что мы одинаково называли папки, в которых лежат файлы и нейсмспейсы классов? Это мы делали не просто так, а для того, чтобы можно было преобразовать полное имя класса (включая его неймспейс) в путь до .php-файла с этим классом.

Итак, давайте сделаем эту функцию автозагрузки. Давайте я сначала приведу пример кода, а затем объясню, как это работает. Наш файл index.php принимает следующий вид:

<?php

function myAutoLoader(string $className)
{
    require_once __DIR__ . '/../src/' . str_replace('\\', '/', $className) . '.php';
}

spl_autoload_register('myAutoLoader');

$author = new \MyProject\Models\Users\User('Иван');
$article = new \MyProject\Models\Articles\Article('Заголовок', 'Текст', $author);
var_dump($article);

А теперь по порядку о том, что же происходит.

  • Функция spl_autoload_register() принимает первым аргументом имя функции, в которую будет передаваться имя класса, каждый раз, когда этот класс ещё не был загружен. Поэтому мы создаём новую функцию myAutoLoader() и указываем это имя в качестве аргумента функции spl_autoload_register().
  • Теперь по поводу функции myAutoLoader. Каждый раз, когда в коде будет встречаться класс, который ещё не был подключён, в неё первым аргументом будет передаваться полное имя класса (вместе с неймспейсом). И мы должны на основе этого полного имени подключить нужный файл. Так как у нас пути до файлов и их неймспейсы совпадают, то мы просто склеиваем строку из следующих составляющих: текущая директория + поднимаемся на уровень выше + переходим в папку src + полное имя класса + добавляем расширение .php. Ну и не забываем заменить обратные слеши из неймспейса на прямые слеши для путей с помощью функции str_replace.

Всё! Теперь все классы будут подгружаться автоматически. Давайте запустим скрипт и убедимся, что всё работает.

Давайте добавим отладочную информацию внутри функции myAutoLoader(), чтобы проверить что всё именно так и работает. Добавим var_dump() с выводом переменной $className.

function myAutoLoader(string $className)
{
    var_dump($className);
    require_once __DIR__ . '/../src/' . str_replace('\\', '/', $className) . '.php';
}

Снова запустим скрипт и посмотрим на вывод.

string 'MyProject\Models\Users\User' (length=20)
string 'MyProject\Models\Articles\Article' (length=26)
object(MyProject\Models\Articles\Article)[2]
  private 'title' => string 'Заголовок' (length=18)
  private 'text' => string 'Текст' (length=10)
  private 'author' => 
    object(MyProject\Models\Users\User)[1]
      private 'name' => string 'Иван' (length=8)

Мы видим, что в эту функцию попал сначала класс MyProject\Models\Users\User, а затем MyProject\Models\Articles\Article. И для этих классов мы сделали require нужных файлов и они успешно подгрузились.

На этом давайте var_dump уберём.

В функцию spl_autoload_register можно и вовсе передать не имя функции, а прямо саму функцию – не будем сейчас на этом останавливаться более детально. Просто знайте, что так можно:

<?php

spl_autoload_register(function (string $className) {
    require_once __DIR__ . '/../src/' . str_replace('\\', '/', $className) . '.php';
});

$author = new \MyProject\Models\Users\User('Иван');
$article = new \MyProject\Models\Articles\Article('Заголовок', 'Текст', $author);
var_dump($article);

В таком случае, функция называется анонимной – у неё нет имени. Она просто передаётся в качестве аргумента и имя ей не нужно.

Запустите код ещё раз, и убедитесь, что всё работает как нужно.
Вот такими вот нехитрыми действиями мы сделали автозагрузку классов. PHP – прекрасный язык, не правда ли?

Начиная с текущего урока я решил выкладывать итоговые результаты уроков на github, чтобы вам в случае чего можно было свериться с кодом, который должен был получиться в конце урока. Вот ссылка на результат этого урока.

loader
Комментарии
Этот урок набрал набрал достаточно большое количество комментариев и дальнейшее его комментирование отключено. Если вы хотели убедиться в правильности выполнения ДЗ или у вас возник вопрос по уроку, посмотрите ранее добавленные комментарии, кликнув по кнопке ниже. Скорее всего вы найдете там то, что искали. Если это не помогло - задайте вопрос в чате в телеграме - https://t.me/php_zone
Логические задачи с собеседований