Pull to refresh

Автозагрузка в PHP: начали за здравие, а кончили за упокой

Reading time 5 min
Views 22K
Original author: Francois Zaninotto

Предисловие переводчика


Данная статья является вольным переводом-пересказом поста The End of Autoloading
. Оригинальная статья не первой свежести, поэтому код приведенный в примерах может быть не актуален. В теме, которую затрагивает статья, самое главное — общий взгляд, а не конкретные примеры.

Предисловие


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


До автозагрузки. Пути к файлам.


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

<?php
require_once 'PEAR.php';
require_once 'PEAR/DependencyDB.php';

class PEAR_Registry extends PEAR {
  //...
}

Зависимости явно прописывались в начале каждого скрипта. В этом примере зависимость очевидна, даже если вызов PEAR_DependencyDB находится на 328 строке.

Приход SPL-автозагрузки.


Позже появилась функция spl_autoload_register(), позволившая убрать вызовы require/require_once. Замечательным было то, что теперь появилась возможность использовать классы без необходимости знания где они расположены. Например:

<?php
class postActions extends sfActions
{
  public function executeList()
  {
    $this->posts = PostPeer::doSelect(new Criteria());
  }
}

Смотрите, здесь нет ни единого вызова require/require_once, при том, что этот класс зависим от sfActions, PostPeer, и Criteria классов. Разработчики смогли заниматься бизнес-логикой, не тратя время на поиск путей зависимостей. Это было действительно удобно.

Реализации автозагрузки.


Реализации автозагрузки варьируются. Некоторые библиотеки используют список всех классов, которые нужно подключить. Например:

<?php
class Propel
{
  // ..
  protected static $autoloadMap = array(
    'DBAdapter'      => 'adapter/DBAdapter.php',
    'DBMSSQL'        => 'adapter/DBMSSQL.php',
    'MssqlPropelPDO' => 'adapter/MSSQL/MssqlPropelPDO.php',
    'MssqlDebugPDO'  => 'adapter/MSSQL/MssqlDebugPDO.php',
    // etc.
}

Эта техника позволила скрыть пути к классам, но заставила разработчика библиотеки обновлять “карту” автозагрузки, каждый раз, при добавлении нового класса. Другая техника использует проход по структуре папок проекта в поиске нужного класса. Такой подход позволил подменять классы фреймворка своими, т.к. проход по папкам происходил в определенном порядке: пользовательские папки, проекта, плагинов и фреймворка. Таким образом разработчик мог создать класс ClassName, который подменит ClassName предоставленный плагином, т.к. загрузится раньше.

Автозагрузка с неймспейсами


Приход неймспейсов изменил техники автозагрузки. Авторы фреймвроков объединились, что бы унифицировать техники автозагрузки, для возможности технического взаимодействия между различными библиотеками. Было решено, что явное лучше неявного и полное имя класса будет относительным путем к файлу.

\Doctrine\Common\IsolatedClassLoader
  => /path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php
\Symfony\Core\Request
  => /path/to/project/lib/vendor/Symfony/Core/Request.php
\Zend\Acl
  => /path/to/project/lib/vendor/Zend/Acl.php
\Zend\Mail\Message
  => /path/to/project/lib/vendor/Zend/Mail/Message.php

Были выработаны принципы именования и файловой структуры, а также реализация класса SplClassLoader. Этот подход сейчас используется практически во всех современных фреймворках. Пример кода:

<?php
namespace Application\HelloBundle\Controller;
use Symfony\Framework\WebBundle\Controller;

class HelloController extends Controller
{
  public function indexAction($name)
  {
    $author = new \Application\HelloBundle\Model\Author();
    $author->setFirstName($name);
    $author->save();
    return $this->render('HelloBundle:Hello:index', array('name' => $name, 'author' => $author));
  }
}

Здесь все также нет require благодаря автозагрузке. Автозагрузчик ищет Symfony\Framework\WebBundle\Controller класс в файле Symfony/Framework/WebBundle/Controller.php.
Все хорошо, все довольны. Зависимости явные и подмена класса происходит легко:

<?php
namespace Application\HelloBundle\Controller;
// use a custom Controller class instead of the framework's Controller
use Application\HelloBundle\Tools\Controller;

class HelloController extends Controller
{
  // same code as before
}


Конец все удобствам.


Использование use в предыдущем примере вам ничего не напоминает? Это ведь очень похоже на старый добрый require/require_once, не так ли?

<?php
// old style
require_once 'Application/HelloBundle/Tools/Controller.php';
// new style
use 'Application\HelloBundle\Tools\Controller';

Привнесенная неймспейсами многословность в первую очередь снижает легкость использования автозагрузки. Но проблема не только в том, что нужно писать больше кода, в этом вам может помочь IDE с автодополнением, а в том, что вам нужно знать полные имена нужных вам классов. Вы должны очень хорошо знать классы фреймворка, чтобы использовать их. Это шаг назад по сравнению с автозагрузкой “первого поколения”, где было достаточно знать имя класса.

Другого пути нет.


Правда было бы замечательно, использовать современные библиотеки без знания их файловой структуры? Что если бы вы могли написать контроллер так:

<?php
class HelloController extends Controller
{
  public function indexAction($name)
  {
    $author = new Author();
    $author->setFirstName($name);
    $author->save();
    return $this->render('HelloBundle:Hello:index', array('name' => $name, 'author' => $author));
  }
}

Умный автозагрузчик перехватил бы вызов класса Controller, загрузил бы файл Symfony/Framework/WebBundle/Controller.php и динамически создал алиас с Symfony\Framework\WebBundle\Controller на Controller. К сожалению в PHP use создает алиас во время компиляции, поэтому такой ход не сработает. Конечно есть возможность сделать подобное используя eval, но это, наверное, даже хуже, чем подключать файлы вручную. Также создание таких алиасов при работе с фреймворком не возможно по причине конфликта с ленивой загрузкой и конфликта имен классов, например:
Symfony\Framework\WebBundle\Command
и
Symfony\Components\Console\Command\Command
Пока авторы фреймворков не изменят свой взгляд на автозагрузку, будущее PHP мне видится многословным.

Решение проблемы.


Лично я, думаю, что многословность сильно замедляет разработку. Например возьмем микрофреймворки – они дают возможность обработать запрос быстро, при минимуме MVC-разделения. Сравним код примерного приложения написанного с использованием Slim (автозагрузка без неймспейсов) и Silex (автозагрузка с неймспейсами):

<?php
// Hello world with Slim
require_once 'slim/Slim.php';
Slim::init();
Slim::get('/hello/:name', function($name) {
    Slim::render('hello.php', array('name' => $name));
});
Slim::run();

// Hello world with Silex
require_once 'silex.phar';
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Templating\Engine;
use Symfony\Component\Templating\Loader\FilesystemLoader;
use Silex\Framework;
$framework = new Framework(array(
  'GET /hello/:name' => function($name) {
    $loader = new FilesystemLoader('views/%name%.php');
    $view = new Engine($loader);
    return new Response($view->render(
      'hello', 
      array('name' => $name)
    ));
  }
));
$framework->handle()->send();

Во втором примере, автозагрузка делает все только сложнее.

Разработчики современных фреймворков объясняют, что многословность – это цена, которую мы платим за качество кода. Я не уверен, что хочу платить эту цену. Я не хочу видеть как PHP превращается в Java, где код превосходен с точки зрения Computer Science, но очень затратен в написании. Это побуждает желание к использованию других языков, где вопрос автозагрузки с неймспейсами не стоит и при этом, быстрая разработка возможна.

Возьмем например Ruby. Существует такой фреймворк как Sinatra, используя который HelloWorld-приложение становится очень лаконичным:

require 'sinatra'
require 'erb'
get '/hello/:name' do |name|
    @name = name
    erb :hello
end

Ой, смотрите, здесь же используется require! И при этом, все пишется очень быстро и легко в использовании.
Tags:
Hubs:
+25
Comments 116
Comments Comments 116

Articles