Основы ООП и контроллеры страниц

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

Ядро — это один основной класс, с общими для всех контроллеров методами. А контроллеры — это другие php классы, которые будут отвечать за функционал какого-то раздела сайта.

Контроллеры будут лежать в специальной директории, откуда их запустит основной класс. Запрос приходит на index.php, тот инициализирует основной класс и просит обработать запрос. Ядро определяет, к какому разделу сайта обращён запрос и загружает его контроллер, передавая ему все полномочия на генерацию ответа.

Дальше контроллер проверит параметры запроса и выдаст результат, или редирект на страницу с ошибкой. Позже мы научим наши контроллеры обрабатывать запросы и выдавать ответы через Ajax.

Логическая цепочка выходит такая: index.php -> Сore -> Controllers_Page.

Что такое ООП

Объектно-ориентированное программирование — это когда вы оперируете не набором файликов php, а набором объектов. Конечно, они располагаются в файлах, но представляют собой не разрозненный набор функций, а готовый кусок какого-то функционала.

В литературе обычно приводят примеры рельных объектов, типа дома или автомобиля, у которых есть свойства (окна, руль, колёса) и методы (включить свет, поехать). Лично мне эти примеры всегда были не очень понятны, поэтому я попробую объяснить принципы работы классов иначе.

Итак, класс — это готовая к употреблению программа, которая выполняет какие-то функции. Вы можете подключить к себе на сайт эту программу, чтобы она выполняла эти функции для вас.

Структурно PHP класс представляет собой всё тот же набор свойств и методов. Свойства — это просто переменные внутри класса и снаружи его методов. А методы — это обычные функции внутри класса.

По сути любой php класс выглядит так:
<?php

class Core {
	public $test = 1;

	public function test() {
		return 'Hello World!';
	}
}

Если здесь убрать объявление класса и слова public — то получится обычный php код, не правда ли? Так зачем вообще нужно заморачиваться с этими классами? А затем, что у них есть замечательные особенности:
  1. Внутри класса всё выполняется изолированно. Переменные и функции внутри класса и снаружи никак не пересекаются и не выдают ошибок о том, что такая функция уже объявлена.
  2. Вы, как автор класса, можете указать, какие методы можно выполнять снаружи, а какие исключительно для внутреннего использования.
  3. Класс может быть расширен другим классом, и его методы могут быть переопределены. Конечно, если вы, как автор класса, это разрешили.
  4. Все методы внутри класса могу обращаться друг к другу без ограничений. Вы моежет хранить внутри класса данные так, чтобы их не было видно снаружи и их нельзя было изменить.
В конце концов класс — это удобная упаковка для готового функционала, которую можно перетаскивать из одного проекта в другой с минимальными изменениями.

Давайте же начнём писать наш первый класс с ядром сайта!

Core.php

Так как мы только начинаем изучать ООП в PHP, мы пока не будем следовать стандартам PSR: использовать пространства имён, автозагрузчик и прочее. Мы просто пишем рабочий код и, возможно, на последнем занятии отрефакторим его правильно.

Итак, создаём директорию /core/, в ней файл Core.php:
<?php

class Core {
	public $config = array();


	/**
	 * Конструктор класса
	 *
	 * @param array $config
	 */
	function __construct(array $config = array()) {
		$this->config = array_merge(
			array(), $config
		);
	}

}
Что здесь происходит?

Во первых — объявление класса:
class Core {}

Внутри класса объявляем публичную переменную config (про области видимости чуть ниже). Она будет нужна нам для выставления разных настроек в работе класса.

Дальше следует специальный метод __construct() — он выполняется всегда один раз, при создании нового экземпляра класса.
Экземпляр класса — это его, как бы, копия, которую мы получаем при инициализации класса и можем использовать как угодно. Можно создавать неограниченное количество экземпляров класса, что есть еще одна гибкость ООП.

При инициализации наш класс будет соединять массив стандартных настроек (пока что пустой) и переданный массив настроек от пользователя. А дальше, внутри класса, можно будет к этим настройкам обращаться через $this->config.

Вообще, все обращения к методам экземпляра класса происходят через $this. То есть, экземпляр обращается, таким образом, сам к себе.

Инициализация класса

Мы уже определились, что вся работа у нас идёт через index.php. Директорию core вообще, по-хорошему, нужно закрыть от всех запросов извне.

Так что, в index.php нам и нужно инициализировать Core:
if (!class_exists('Core')) {
	require_once 'core/Core.php';
}
$Core = new Core(array('test' => 'Yes!'));

print_r($Core->config);
Здесь мы, на всякий случай, проверяем, вдруг класс Core уже был загружен, и если нет — подключаем файл с ним. Затем создаём новый экземпляр класс командой new и передаём наш массив параметров конструктору.

После этого мы можем распечатать конфиг этого экземпляра, он нам выведет
Array ( [test] => Yes! )

И вот теперь смотрите, как можно делать с классами:
$Core1 = new Core(array('test' => 'Yes!'));
$Core2 = new Core(array('test' => 'No!'));
print_r($Core1->config);
print_r($Core2->config);
На выходе:
Array ( [test] => Yes! ) Array ( [test] => No! )

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

Теперь про области видимости. Переменные и методы внутри класса могут быть трёх разных видов:
  1. public — любой скрипт снаружи может прочитать и записать это свойсто, или выполнить метод.
  2. protected — доступ есть только у класса, который наследует и расширяет наш класс.
  3. private — доступ есть только у самого класса
Таким образом, когда мы объявили переменную $config как public — мы разрешили доступ к ней снаружи. Поэтому index.php может распечатать массив с настройками.

Давайте теперь объявим новый публичный метод handleRequest, куда перенесём логику по обработке запроса из index.php.

Метод обработки запроса

Пишем в Core.php:
	/**
	 * Обработка входящего запроса
	 *
	 * @param $uri
	 */
	public function handleRequest($uri) {
		// Массив доступных страниц
		$pages = array('home', 'test');
		// Определяем страницу для вывода
		$page = '';
		// Если запрос не пуст - проверяем, есть ли он в массиве наших страниц

		$request = explode('/', $uri);
		// Если есть - окей, всё верно, используем это имя
		if (in_array(strtolower($request[0]), $pages)) {
			$page = strtolower($request[0]);
		}
		// Иначе используем страницу по умолчанию
		if (empty($page)) {
			$page = 'home';
		}

		echo "Мы выводим страницу <b>{$page}<b>";
	}
Как видите, мы указали в методе обязательный параметр uri, который должен передать вызывающий скрипт. В нашем случае, это index.php, который мы меняем вот так:
if (!class_exists('Core')) {
	require_once 'core/Core.php';
}
$Core = new Core();

$req = !empty($_REQUEST['q'])
	? trim($_REQUEST['q'])
	: '';
$Core->handleRequest($req);
Он проверяет, есть ли в запросе от сервера наша переменная q, и если нет — то запрошена корневая страница. Дальше в handleRequest передаётся или запрос или пустота, пусть уже он сам разбирается. Проверка на существование $_REQUEST['q'] нужна для того, чтобы не было обращения к несуществующему элементу массива и E_NOTICE вслед за этим (которую мы увидим, потому что заранее включили вывод всех сообщений в index.php на прошлом уроке).

Выгружаем всё на сервер и проверяем. Должно быть тоже самое, что и раньше:
Мы выводим страницу home
по адресу s1889.h3.modhost.pro/ и
Мы выводим страницу test
по адресу s1889.h3.modhost.pro/test/

Зачем вообще эта чехарда с новым классом и специальным методом в нём для обработки запроса, ведь мы могли оставить всё это и в index.php?

Нет, не могли, потому что мы не хотим забивать намертво все возможные разделы сайта.
Мы хотим сделать отдельную директорию с контроллерами этих разделов, и доработать наш handleRequest таким образом, чтобы он сам проверял — есть ли обработчик для запроса, или нет? Если есть, запускал бы его, а если нет — выводил ошибку 404.

На данном этапе исходники нашего сайта выглядят вот так.

Контроллеры разделов

Давайте создадим новую директорию /core/controllers/ и добавим в неё класс Home.php:
<?php

class Controllers_Home {
	/** @var Core $core */
	public $core;


	/**
	 * Конструктор класса, требует передачи Core
	 *
	 * @param Core $core
	 */
	function __construct(Core $core) {
		$this->core = $core;
	}


	/**
	 * Основной рабочий метод
	 *
	 * @return string
	 */
	public function run() {
		return "Мы выводим страницу <b>Home<b>";
	}

}
А рядом создадим файл такой же файл Test.php, с чуть изменённым методом run():
	public function run() {
		return "Мы выводим страницу <b>Test<b>";
	}

Как видите, эти 2 контроллера требуют, чтобы им при инициализации был передан экземпляр класса Core. Это прописано у них в методе __construct(Core $core);.

Теперь нам нужно доработать наш метод handleRequest, чтобы он сам определял, какой раздел сайта у нас запросили и передавал работу нужному контроллеру.
	/**
	 * Обработка входящего запроса
	 *
	 * @param $uri
	 */
	public function handleRequest($uri) {
		// Определяем страницу для вывода
		$request = explode('/', $uri);
		// Имена контроллеров у нас с большой буквы
		$name = ucfirst($request[0]);
		// Полный путь до запрошенного контроллера
		$file = $this->config['controllersPath'] . $name . '.php';
		// Если нужного контроллера нет, то используем контроллер Home
		if (!file_exists($file)) {
			$file = $this->config['controllersPath'] . 'Home.php';
			// Определяем имя класса, согласно принятым у нас правилам
			$class = 'Controllers_Home';
		}
		else {
			$class = 'Controllers_' . $name;
		}
		// Если контроллер еще не был загружен - загружаем его
		if (!class_exists($class)) {
			require_once $file;
		}
		// И запускаем
		/** @var Controllers_Home|Controllers_Test $controller */
		$controller = new $class($this); // Передавая экземпляр текущего класс в него - $this
		$response = $controller->run();

		echo $response;
	}
Внимательно читаем код и комментарии, проникаясь идеей: контроллер загружается автоматически, в зависимости от запроса, и вся дальнейшая работа передаётся ему.

Вот, у нас уже есть разделы сайта! Каждый контроллер имеет доступ к основному классу через свою переменную $this->core, которая выставляется при его инициализации. Если попробовать запустить контроллер без Core — будет ошибка.

Снаружи на сайте всё выглядит как и раньше, выводятся 2 надписи. Но вы уже знаете, что за каждую надпись отвечает свой контроллер. Если мы захотим, то можем добавить новую страницу /news/, для работы которой нужно будет создать соответствующий контроллер /controllers/News.php — и никаких изменений в Core и его handleRequest не понадобится!

Если же юзер запросит несуществующую страницу (то есть такую, для которой нет контроллера), то он получит страницу Home. Пока без кода 404, его мы добавим позже.

Вы должны были заметить, что в handleRequest я обращаюсь в $this->config за указанием пути к контроллерам. Да, всё верно, я немного изменил его __construct() вот так:
	function __construct(array $config = array()) {
		$this->config = array_merge(
			array(
				'controllersPath' => dirname(__FILE__) . '/controllers/',
			),
			$config
		);
	}
То есть, указал директорию по умолчанию, в которой лежат контроллеры. Она берётся от директории текущего файла (а это Core.php, потому что всё дело происходит в нём) и index.php теперь может переопределить путь к контроллерам сайта, если захочет. В Core у нас от этого ничего не сломается.

Еще, возможно вас гложет вопрос, что это странные такие комментарии перед объявлением методов и инициализацией переменных, типа:
/** @var Controllers_Home|Controllers_Test $controller */
Очень просто — это комментарии в формате PHPDoc, чтобы будущим пользователям вашего кода и вашей IDE было понятно, что вы имеете в виду в тех или иных местах.

PhpStorm умеет генерировать эти комментарии самостоятельно, а вам я советую прочитать о них на Википедии или на официальном ресурсе.

Заключение

Вот мы и написали с вами сразу аж 3 класса и научились их использовать для обработки запросов страниц.

На данный момент у нас есть index.php, основной класс Core с настройками и 2 контроллера для обработки страниц: Controllers_Test и Controllers_Home. Если запрошенной страницы на сайте нет, то выводится Home.

На следующем уроке мы напишем базовый контроллер, куда сложим все основные методы обработки страниц, а Test и Home будут его расширять. Это сэкономит нам кучу времени и байтов, потому что нам не придётся дублировать один и тот же код в двух разных файлах.

Надеюсь, вам всё понятно. Если же нет — жду комментариев с вопросами. На данном этапе наш исходный код выглядит вот так.

Следующая заметка
Базовый контроллер и его методы
Предыдущая заметка
Вводное занятие


Комментарии (15)

  1. Перетягин Илья 27 июня 2015, 20:02 # 0
    function __construct(array $config = array())
    Понятно, что это метод (функция), понятно, что такое конструкт, но вот, что в скобках… просто не понимаю зачем и что делает.

    Или вот еще момент
    $Core = new Core(array('test' => 'Yes!'));
    Я так понимаю, таким способом мы передаем информацию, но как получается, что она попадает именно в
    public $config = array();
    И таких вопросов много, если их все задавать…
    1. Василий Наумкин 27 июня 2015, 20:38 # 0
      В скобках функции указываются параметр, которые она принимает.

      array $config = array() — это приём параметра $config, который может быть только массивом и по умолчанию — пустой массив.

      Если передать в функцию строку или число (что угодно, кроме массива) — будет ошибка.
      А если не передавать ничего, то $config внутри метода будет пустым массивом.

      Собственно, при вызове
      $Core = new Core(array('test' => 'Yes!'));
      Мы и передаём массив параметров, который внутри __construct становится переменной $config — как и написано в объявлении фунцкии.
      1. Перетягин Илья 27 июня 2015, 20:47 # 0
        Получается так:
        array $config = array()
        array — это условие, что может принимать метод, а
        $config = array() — это значение по умолчанию?

        $Core = new Core(array('test' => 'Yes!'));
        Такая конструкция передаст параметры всегда в __construct?
        1. Василий Наумкин 27 июня 2015, 22:20 # +1
          function __construct(array $config = array())
          $config — это переменная, которую принимает конструктор класса
          = array() — это её значение по умолчанию, если мы ничего не передаём
          а начальный array — это требование того, что передаваемые данные могут быть только массивом.

          Можно указать и так:
          function __construct($config = array())
          И тогда функция примет любую переменную, не только массив

          А можно и так:
          function __construct($config)
          и тогда функция будет требовать передать ей что-то на вход, иначе ошибка (потому что нет значения по умолчанию) и класс не запустится.
          1. Перетягин Илья 27 июня 2015, 22:37 # 0
            Спасибо, сейчас все понятно!
    2. Андрей Кухарев 06 августа 2015, 00:05 # 0
      Василий, с помощью php.net вроде разобрался.
      правильно понял что при запуске Core.php сначала поместили массив из файлов директории /controllers/ в переменую config, а в функции handleRequest сначала преобразовываем запрос пользователя под тип значений массива, и проверяем есть ли такой -> выдаем нужный контроллер директории сайта.
      так долго разбирался с синтаксисом наверно мне нужен какой то задачник (как в школе по математике) для изучения этого множества функций как раз твой курс будет первым
      1. Василий Наумкин 06 августа 2015, 05:24 # 0
        Не совсем так. При инициализации класса в конфиг мы передаём только строку с путём до директории контроллеров.

        А дальше метод handleRequest при обработке запроса смотрит, какие в этой директории есть файлы, выбирает подходящий контроллер и передаёт работу ему. Если подходящего контроллера нет — то выдаёт ошибку.

        Передавать готовый список контроллеров не очень удобно. Гораздо приятнее просто добавлять или удалять файлы контроллеров в одной директории, без изменения кода handleRequest.

        1. Андрей Кухарев 06 августа 2015, 08:57 # 0
          спасибо! ещё понятнее, ночью как то по другому работает голова :)
      2. Николай Савин 25 декабря 2015, 13:00 # 0
        Чуть подзапоздал с изучением курса, но все таки добрался. Надеюсь вопросы еще уместны и будет время, чтобы на них ответить.

        
        function _construct(Core $core) { }		
        
        Насколько я понял, этой строкой мы подключаем Ядро, и далее будем его расширять.
        Слово Core это какой то встроенный в ядро PHP оператор? Как он работает?
        Выше был пример с array — там было понятно, что принимаются только массивы, а строки и числа заворачиваются и идут лесом.

        Далее что конкретно должно приходить в $core? Какие данные?
        1. Николай Савин 25 декабря 2015, 13:06 # 0
          Ответ на второй вопрос нашел но не до конца понял
          $controller = new $class($this); // Передавая экземпляр текущего класс в него - $this
          При вызове класса Controller_Home в него передается экземпляр Ядра.
          Он передается в виде объекта? Так что становятся доступны все его методы и свойства. Верно я понимаю?
          1. Василий Наумкин 25 декабря 2015, 13:35 # 0
            Верно, да.

            И в конструкторе контроллера требуется именно экземпляр класса Core, если передать что-то другое, то будет ошибка и контроллер не запустится.

            То есть, мы еще на стадии создания контроллера требуем передачи в него только определённого класса, и класс этот — Core, то бишь наше ядро.
            1. Николай Савин 25 декабря 2015, 13:42 # 0
              А не проще написать что то типа:?
              class Myclass extends Core
              Это даст тот же эффект?
              1. Василий Наумкин 25 декабря 2015, 13:45 # 0
                Это расширение одного класса другим, используется обычно для изменения работы методов. В принципе, можно и так, но это менее гибко.

                Мы же передаём один класс в другой и отделяем таким образом контроллер от ядра. Дальше будет понятнее, в чём удобство.
              2. Адиль 30 января 2016, 13:41 # 0
                Василий при $class = 'Controllers_Home'; понятно что в $controller = new $class($this); будет передан объект класса Core
                и при $class='Controllers_Test' понятно что в $controller = new $class($this); будет храниться объекта класса Controllers_Test ну вот что передается в качестве параметра не совсем понятно, что значит ($this)? при значении $class='Controllers_Test' это значение публичного члена $core? или что, не совсем понял.
                1. Василий Наумкин 30 января 2016, 13:44 # 0
                  $this — это всегда экземпляр текущего класса, внутри которого происходит действие.

                  В данном случае да, это экземпляр Core, потому что действие происходит внутри него.
          Добавление новых комментариев отключено.