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

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

Ядро - это один основной класс, с общими для всех контроллеров методами. А контроллеры - это другие 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

по адресу http://s1889.bez.modhost.pro и

Мы выводим страницу test

по адресу http://s1889.bez.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 будут его расширять. Это сэкономит нам кучу времени и байтов, потому что нам не придётся дублировать один и тот же код в двух разных файлах.

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

← Предыдущая заметка
Вводное занятие
Следующая заметка →
Базовый контроллер и его методы
Комментарии (24)
OnFoxПеретягин Илья
27.06.2015 17:02
function __construct(array $config = array())

Понятно, что это метод (функция), понятно, что такое конструкт, но вот, что в скобках... просто не понимаю зачем и что делает.

Или вот еще момент

$Core = new Core(array('test' => 'Yes!'));

Я так понимаю, таким способом мы передаем информацию, но как получается, что она попадает именно в

public $config = array();

И таких вопросов много, если их все задавать....

bezumkinВасилий Наумкин
27.06.2015 17:38

В скобках функции указываются параметр, которые она принимает.

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

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

Собственно, при вызове

$Core = new Core(array('test' => 'Yes!'));

Мы и передаём массив параметров, который внутри __construct становится переменной $config - как и написано в объявлении фунцкии.

OnFoxПеретягин Илья
27.06.2015 17:47

Получается так:

array $config = array()

array — это условие, что может принимать метод, а $config = array() - это значение по умолчанию?

$Core = new Core(array('test' => 'Yes!'));

Такая конструкция передаст параметры всегда в __construct?

bezumkinВасилий Наумкин
27.06.2015 19:20
function __construct(array $config = array())

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

Можно указать и так:

function __construct($config = array())

И тогда функция примет любую переменную, не только массив

А можно и так:

function __construct($config)

и тогда функция будет требовать передать ей что-то на вход, иначе ошибка (потому что нет значения по умолчанию) и класс не запустится.

OnFoxПеретягин Илья
27.06.2015 19:37

Спасибо, сейчас все понятно!

Адиль
30.01.2016 05:51

Василий массив $config указанный в методе __construct и публичный член класса $config который объявлен вами ранее это одно и тоже или что?

Адиль
30.01.2016 05:55

затем далее ты в теле метода __construct пишешь


$this->config = array_merge(
            array(), $config

что за массив array() который ты сливаешь с массивом $config откуда ты его берешь и откуда он поступает в метод construct?

bezumkinВасилий Наумкин
30.01.2016 06:00

Адиль, давай я лучше тебе сразу верну деньги?

Судя по уровню вопросов, ты мне мозг вынесешь так, что я прокляну эти 2000 руб.

Адиль
30.01.2016 06:07

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

bezumkinВасилий Наумкин
30.01.2016 06:11

Вот что здесь непонятно и заставило тебя задать вопрос?

затем далее ты в теле метода __construct пишешь ... что за массив array() который ты сливаешь с массивом $config откуда ты его берешь и откуда он поступает в метод construct?

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

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

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

Я делаю вывод что ты не читаешь, или не хочешь понимать мой текст. Соотвественно, мне проще вернуть тебе сразу деньги, чтобы не иметь таких бесед как сейчас. Я просто больше времени и нервов потрачу.

Адиль
30.01.2016 06:14

Василий я прочел и читаю по несколько раз то что ты пишешь, поверь мне на слово и то что написано тобою я тоже прочел. Просто в коде твоем я ни где еще не встретил то что ты передаешь пользовательский массив array в конструктор класса. Как я понял чтоб мне тебя не злить мне надо сначала прочесть весь курс и при повторном прочтении задавать вопросы, так можно?

bezumkinВасилий Наумкин
30.01.2016 06:18

Причем здесь "злить"? Я уже пообщался с тобой на modx.pro, я уже пообщался с тобой на modstore.pro и сейчас общаюсь на bezumkin.ru.

Из этого общения я делаю вывод, что ты не знаешь PHP, вообще, и очень хочешь научиться. Но у меня нет ни времени, ни желания учить тебя с нуля. Судя по твоему объявлению - никто не хочет этого делать.

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

Адиль
30.01.2016 06:21

Хорошо Василий.

Адиль
30.01.2016 06:15

Василий не злись пожалуйста, я же набираю знания может ты более умнее чем я и все схватываешь на лету, просто у меня такая шиза всегда присутствовала я люблю глубоко погружаться и если встречаю в коде что то мне не понятное освоение материала тормозится

krashАндрей Кухарев
05.08.2015 21:05

Василий, с помощью http://php.net вроде разобрался. правильно понял что при запуске Core.php сначала поместили массив из файлов директории /controllers/ в переменую config, а в функции handleRequest сначала преобразовываем запрос пользователя под тип значений массива, и проверяем есть ли такой -> выдаем нужный контроллер директории сайта. так долго разбирался с синтаксисом наверно мне нужен какой то задачник (как в школе по математике) для изучения этого множества функций как раз твой курс будет первым

bezumkinВасилий Наумкин
06.08.2015 02:24

Не совсем так. При инициализации класса в конфиг мы передаём только строку с путём до директории контроллеров.

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

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

krashАндрей Кухарев
06.08.2015 05:57

спасибо! ещё понятнее, ночью как то по другому работает голова :)

biz87Николай Савин
25.12.2015 10:00

Чуть подзапоздал с изучением курса, но все таки добрался. Надеюсь вопросы еще уместны и будет время, чтобы на них ответить.



function _construct(Core $core) { }		

Насколько я понял, этой строкой мы подключаем Ядро, и далее будем его расширять. Слово Core это какой то встроенный в ядро PHP оператор? Как он работает? Выше был пример с array - там было понятно, что принимаются только массивы, а строки и числа заворачиваются и идут лесом.

Далее что конкретно должно приходить в $core? Какие данные?

biz87Николай Савин
25.12.2015 10:06

Ответ на второй вопрос нашел но не до конца понял

$controller = new $class($this); // Передавая экземпляр текущего класс в него - $this

При вызове класса Controller_Home в него передается экземпляр Ядра. Он передается в виде объекта? Так что становятся доступны все его методы и свойства. Верно я понимаю?

bezumkinВасилий Наумкин
25.12.2015 10:35

Верно, да.

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

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

biz87Николай Савин
25.12.2015 10:42

А не проще написать что то типа: ?

class Myclass extends Core

Это даст тот же эффект?

bezumkinВасилий Наумкин
25.12.2015 10:45

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

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

Адиль
30.01.2016 10:41

Василий при $class = 'Controllers_Home'; понятно что в $controller = new $class($this); будет передан объект класса Core и при $class='Controllers_Test' понятно что в $controller = new $class($this); будет храниться объекта класса Controllers_Test ну вот что передается в качестве параметра не совсем понятно, что значит ($this)? при значении $class='Controllers_Test' это значение публичного члена $core? или что, не совсем понял.

bezumkinВасилий Наумкин
30.01.2016 10:44

$this - это всегда экземпляр текущего класса, внутри которого происходит действие.

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

bezumkin
Василий Наумкин
04.07.2022 23:34
Что-то странное у тебя произошло: миграция есть, и вроде как выполнена, но таблицы при этом отсутств...
inetlover
Александр Наумов
03.07.2022 20:36
Василий, спасибо! Все понятно!
bezumkin
Василий Наумкин
02.07.2022 20:28
Спасибо, поправил!
bezumkin
Василий Наумкин
30.06.2022 03:58
Есть ли возможность формировать &quot;friendly URL aliases&quot;, используя аналог translit MODx? ...
bezumkin
Василий Наумкин
27.06.2022 03:32
Спасибо за исправления, очень выручаешь =) Но учитывая количество не описаных в заметке дополнительн...
bezumkin
Василий Наумкин
27.06.2022 03:10
что будет использоваться для вывода многоуровневого меню Посмотри как работают комментарии на этом ...
bezumkin
Василий Наумкин
25.06.2022 11:56
Поправил, спасибо!
bezumkin
Василий Наумкин
21.06.2022 01:58
onLoad(data) { this.total = data.total }, и onLoad({total}) { this.total = total }, В нашем случ...
bezumkin
Василий Наумкин
20.06.2022 14:01
Прекрасно тебя понимаю, я когда сам в этом разбирался - голова дымилась. Но зато теперь прямо-таки п...
bezumkin
Василий Наумкин
20.06.2022 09:30
Не надо, оно по умолчанию так - я просто чуть более подробно написал.