Привет, хабр! В этой статье я хочу написать о том, как сделать PHP-приложение с помощью схемы разделения данных приложения MVC.

Итак, начнем!

Для работы нам нужна функция под названием autoload. Она избавляет нас от бесконечных require'ов. Мы можем вручную написать скрипт, но эта функция есть у знаменитого пакетного менеджера composer.

После установки инициализируем его в главной папке нашего мини-фреймворка командой composer init. на всех вопросах нажимаем ENTER.

Далее заходим в появившийся файл composer.json. Удаляем все и добавляем это:

{
  "name": "<название/пакета>",
    "autoload": {
        "psr-4": {
            "app\\": "./"
        }
    },
    "require": {}
}

И наконец, выполняем команду composer update. На этом настройка composer завершена.

Теперь, создадим папку core и файл .htaccess. Еще нужно создать папку public и создать в ней файл index.php - точку входа в приложение.

В файл .htaccess нужно вписать следующее:

RewriteEngine on

RewriteRule .* public/index.php

Все, что делает этот файл - переадресовывает любые запросы в index.php.

Потом, в папке core создаем класс Application. Помещаем в него этот код:

<?php
namespace app\core;

class Application
{
	
}

Здесь мы создаем пока что пустой класс и пространством имен app\core.

На этом этапе структура должна выглядеть так:

│ .htaccess
│ composer.json
│ composer.lock

├───core
│ Application.php

├───public
│ index.php

└───vendor

Отлично. Теперь давайте выполним первую задачу: маршрутизацию.

Маршрутизация

В папке core создаем класс Router.php и начнем писать код:

<?php
namespace app\core
  
class Router
{
  protected array $routes = [];
  
  public function get($path, $callback)
  {
  	$this->routes['get'][$path] = $callback;
  }

  public function post($path, $callback)
  {
  	$this->routes['post'][$path] = $callback;
  }
}

Мы создали переменную routes, в котором будут храниться все маршруты в таком формате:

// ['method' => ['path' => callback]]

Теперь нужно создать класс Request.php для получения урлов, методов запроса и так далее:

<?php


namespace app\core;


class Request
{
    public function getPath()
    {
        $path = $_SERVER['REQUEST_URI'] ?? '/';
        $position = strpos($path, '?');

        if ($position === false) return $path;
        return substr($path, 0, $position);
    }

    public function getMethod()
    {
        return strtolower($_SERVER['REQUEST_METHOD']);
    }
}

Тут все просто: метод getPath служит для получения url без GET-параметров, а getMethod просто возвращает HTTP-метод. Модифицируем класс Роутера:

<?php
namespace app\core;
  
class Router
{
  protected array $routes = [];
  public Request $request;
  
  public function __construct()
  {
  	$this->request = new Request();
  }

  
  public function get($path, $callback)
  {
  	$this->routes['get'][$path] = $callback;
  }

  public function post($path, $callback)
  {
  	$this->routes['post'][$path] = $callback;
  }
  
  public function resolve()
  {
        $path = $this->request->getPath();
        $method = $this->request->getMethod();
        $callback = $this->routes[$method][$path] ?? false;

        if ($callback === false) {
            return "404";
        }

       return call_user_func($callback);
  }
}

Тут мы создаем экземпляр Request'а и метод resolve, который возвращает то, что вернул callback.

Возвратимся в класс Application и создаем метод run, который запустит Роутер:

<?php


namespace app\core;


class Application
{
    public Router $router;

    public function __construct($ROOT_DIR)
    {
        $this->router = new Router();
    }

    public function run()
    {
        echo $this->router->resolve();
    }
}

Теперь, можем протестировать текущий функционал. Создадим папку public и в ней файл index.php:

<?php
require dirname(__DIR__) . '/vendor/autoload.php';

use app\core\Application;

$app = new Application();

$app->router->get('/', function () {
    return "Hello, habr!";
});

$app->run();

В этой же папке запустим команду php -S localhost:8080 (или любой другой порт). Зайдем на localhost:8080 в браузере и о чудо! Мы увидим надпись Hello, habr!

Итоги

На этом первая часть подходит к концу. В ней мы задали каркас нашего будущего фреймворка. В следующей части мы реализуем View и Контроллеры

Это моя первая статья на Хабре, поэтому буду рад любой критике. Спасибо за прочтение и удачи!

Исходники

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


  1. ponich
    10.03.2022 15:05
    +9

    PSR будет потом?


  1. wendel
    10.03.2022 16:08
    -3

    Опять? ОПЯТЬ! Это не фреймворк, это даже не библиотека, это - HELLO WORLD на PHP!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!


    1. klassev
      10.03.2022 19:12
      +1

      думаете уже обогнали по количеству ботов на пайтоне? ))


      1. wendel
        10.03.2022 20:57

        думаю что все еще только впереди!!! смешат коменты про psr и di... лол, остается только подождать вечность как yii3^2 (:


        1. wendel
          10.03.2022 21:00

          и еще смешно то, что я даже минус поставить не могу, хотя правда на моей стороне.


    1. FanatPHP
      12.03.2022 14:50
      +1

      Вот ниже у вас нормальный разбор. А этот комментарий очень плохой.


      Нет абсолютно ничего плохого в статьях про Hello world. Это никак не может быть аргументом против какой-либо статьи. Как раз наоборот — статей начального уровня нужно больше, особенно для языков типа РНР, в котором все статьи начального уровня застряли в 20 веке и там совсем уж хтонический ужас.


      Любая технология развивается, развиваются и представления о ней. Поэтому, повторюсь — новые статьи на ту же тему, если они доносят её лучше, нужны и должны приветствоваться. А если есть претензии к качеству материала, то вот их как раз можно и нужно высказывать.


  1. pfffffffffffff
    10.03.2022 19:29

    DI будет?


  1. dpts
    10.03.2022 21:40

    Да очередной Hello, World, но полезный для таких неучей, как я, например.

    Реакция "гуру", познавших дзен - весьма странная.

    Автор, продолжайте. Вполне интересно.

    Вот только Fatal error: Uncaught ArgumentCountError: Too few arguments to function app\core\Application::__construct()

    $ROOT_DIR - а- то конструктору не даем нигде.


    1. t38c3j
      10.03.2022 23:24

      А что вам мешает посмотреть реализацию популярных мини фреймворков на гитхаб?


      1. dpts
        10.03.2022 23:43

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


        1. pbatanov
          11.03.2022 15:38

          Рекомендую кстати всегда вот этот цикл прочесть. https://symfony.com/doc/current/create_framework/index.html

          Дает в целом понимание что и как устроено в больших фреймворках и в частности подводки к архитектуре симофни


      1. dpts
        11.03.2022 05:45

        Посмотрел на slim. Таже - фигня в плане логики. Нифига мой мозг не принимает подход, в котором мы:

        1. При получении запроса делаем $app->router->get(маршрут, обработчик маршрута ) , то-есть,

        2. Мы при получении запроса СОЗДАЕМ и маршрут и его обработчик.

        3. При этом создаваемый маршрут никоим образом не связан с запросом.

        4. И тут же этот маршрут обрабатываем.

          вместо того, чтобы сделать $handlerResult = $app->routeHandler($app->router->get($app->request->getPath());

        То-есть вопрос-то не конкретно к приведенному автором примеру, а в принципе почему так? я это к чему, вот есть у меня, например, 50 страниц сайта с единой точкой входа. Моя прямолинейная логика говорит примерно следующее:

        1. мне надо создать массив маршрутов, вида

        [
        	'маршрут1' => Array(параметры маршрута1),
          'маршрут2' => Array(параметры маршрута2),
          и т.д.
        ]
        1. при получении запроса напрмер localhost:8080/маршрут3, получить путь 'маршрут3' из URI

        2. поискать путь в маршрутах и получить параметры обработки маршрута

        3. обработать маршрут и выдать результат.

        То есть по факту 3 вопроса пока по крупному:

        1. Почему маршрут никак очевидным образом не ориентированный на URI СОЗДАЕТСЯ при получении запроса, а не ИЩЕТСЯ на основании URI запроса?

        2. Почему маршрут Создается вместе с обработчиком (что для 50 страниц сайта надо 50 обработчиков/ 50 ссылок на обработчик в маршрутах? А если обработчик "сложносочиненный"? А сложносочиненному обработчику сразу надо дать все данные для обработки - таки их же для начала построить надо из чего-то?)?

        3. Для чего при каждом запросе СОЗДАВАТЬ маршрут заново?


        1. pbatanov
          11.03.2022 13:57

          $app->router->get(маршрут, обработчик маршрута )

          Это не создание маршрута - это фактически конфигурация приложения. То, что его явно нужно конфигурировать кодом каждый раз - это особенность, но фактически это так или иначе происходит во всех фреймворках, просто условный symfony читает кэш DI\роутинга (или строит его по YAML конфигам) неявно для вас, но он тоже это делает это каждый раз при запуске.

          Здесь же вы просто явно выполняете это действие. Для новичков на самом деле эта явность кмк очень полезна.

          Если вы будете запускать Slim на каком-нибудь RoadRunner, то вы явно увидете разницу между конфигурированием и запуском. Конфигурирование (get) будет вне цикла воркера, а запуск внутри.

          пример:

          конфигурация https://github.com/n1215/roadrunner-docker-skeleton/blob/slimphp/app.php

          запуск https://github.com/n1215/roadrunner-docker-skeleton/blob/slimphp/worker.php


          1. dpts
            11.03.2022 14:14

            Вот теперь стало понятнее. с одной стороны. а с другой, появляется глобальный вопрос, для чего так мощно усложнять?.

            Почему от этого всего не оставить только :

            $request = new Request();
            $path = $request->getPath();

            А дальше примитивная магия:

            Это для примераswitch ($path){
            	case '\':
              	echo('Hello, World!');
                break;
              case '\users':
                echo('USERS');
                break;
              case '\users\ivan':
              	echo('Hello, Ivan');
                break;
              default:
              	echo('404');
                break;
            }

            Это для примера. а не в качестве настойчивости на своей точке зрения.

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

            Пока для меня примеры выглядят как попытка забить гвоздь перфоратором.


            1. pbatanov
              11.03.2022 14:59

              Так в целом можно. Но это будет работать только для достаточно простых кейсов. Давайте я добавлю вам требование:

              Для всех роутов с префиксом \users требовать авторизацию.

              И все эти сложные конфиги сразу заиграют новыми красками


              1. dpts
                11.03.2022 15:51

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


                1. pbatanov
                  11.03.2022 16:06

                  Вопрос именно в том, как вы со switch-case навесите авторизацию на группу роутов. Конечно, если вас устраивает на таких объемах каждый роут описывать полностью вручную (вот этот с авторизацией, этот без, этот html с темплейтом и учетом локалей, этот json сырой, этот такой же как вот этот, только без параметров, вместо них вот такие дефолты и тд), то вам все это не надобно, безусловно. вам вообще в этом случае скорей всего хватит пары-тройки .php файлов с инклудами (это кстати описано в цикле статей, который я линканул выше)

                  Понятно что не надо забивать гвозди шуруповертом. простые одноразовые задачи можно решать без всего этого.


                  1. dpts
                    11.03.2022 18:53

                    Вот и я об том, что использовать что-то надо в соответствии с задачей. Не более.

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

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


    1. wendel
      10.03.2022 23:33
      +2

      Поясню свою точку зрения, у меня есть теория. Автор решил изучить php, на глаза ему попались микрофреймворки, в силу своей неопытности он не смог разобраться как они работают но очень вдохновленный красотой запуска приложения решил сделать сие чудо. В чем суть моей претензии, в том что пример ужасный. Допустим у вас есть задача, вам нужно выдатаь некий ответ по http. По сути тот код что написал автор вообще не нужен.
      1. для реквестов респонсов на php уже куча решений, от простых до сложных. Свой велосипед уже тысячу раз объясняли почему не нужен, если вы крупный ентерпрайз и хотите все свое это "ок", но как обычный девелопер возьмите хотя бы psr-15 любое решение из уже существующих. я уж не говорю про компоненты ларавел и симфони если религия позволяет.
      У автора в классе Router создается в !! конструкторе !! объект реквеста, правильно было бы вообще не завязывать класс роутера на реквест, а просто передавать его из RequestHandler как аргумент:
      public function resolve(Request $request)
      и все роутер не зависит от реквеста, это называется лень.

      class Application
      {
          public Router $router;
      
          public function __construct($ROOT_DIR)
          {
              $this->router = new Router();
          }
      
          public function run()
          {
              echo $this->router->resolve();
          }
      }

      Тут я вообще молчу, верх быдлокода. Какой смысл в переменной $ROOT_DIR которая не используется, какой смысл в этом классе если можно сделать просто:

      $request = new Request();
      (new Router())->resolve($request);
      Этот класс просто запускает другой класс, нужно больше абстракций СЭР!!!

      про то что на дворе 2022 год PHP 8.1 а в данном коде ни намека на строгую типизацию, вы в добавок учите УСТАРЕВШИЙ ДИАЛЕКТ ЯЗЫКА который если вы сейчас изучаете конечно полезно для легаси проектов, но как автор "фреймворков" вы отстаете от всех лет на 5, ну да работает не трогай верно.

      Если вы такое покажете на собеседование вас не то что не возьмут, угарать с вас будут. А если возьмут тимлиды там отбитые конечно (: (не на практику а на фуллтайм)


      1. dpts
        10.03.2022 23:44

        Ну вдруг там дальше все наладится и станет хорошо, а этот пост "затравочный".


  1. dpts
    10.03.2022 22:28

    Ну и еще сразу вопросы. Возможно, конечно моя логика вывернута, но:

    1. Условно есть запрос, ну например, localhost:8080/contact/all

    2. Есть кусок кода, $app->router->get('/contact/', ... ); в index.php

    Кусок кода из пункта 2 создает в $routes маршрут.

    В моем понимании логично было бы get-том что-то получать, а не создавать.

    Ну и в свете этого:

    - может Request имеет смысл внутрь Application?

    - и $app->router->get($app->request->getPath(), ...); и чтоб он не создавал маршрут, а отдавал что-то ?

    - А $routes в Router создать загодя?


  1. FanatPHP
    11.03.2022 18:00
    +4

    Статья плохая, но не потому что код примитивный или тема заезженная.
    А потому что это не статья, а репозиторий какой-то.


    Фактически здесь только код, с небольшими перебивками типа "а теперь сделаем то-то". Зачем, почему, какое отношение это все имеет к MVC — непонятно. "Сделаем роутер"! Что такое роутер? Где он находится в буквах MVC? Зачем он нам нужен? Нет ответа. Делаем потому что делаем. "Я дерусь… просто потому что дерусь!" (с) Портос.


    То же самое про реквест. Какое место он занимает в MVC? Почему мы его создаём? Нет ответа. При том что ценность подобной статьи как раз не в ученическом коде, который никто никогда не будет использовать, а именно в объяснении всех тех действий, которые мы совершаем.


    Но видно, что автор и сам не до конца понимает смысла своих действий, и поэтому объяснить их не может. Отсюда появляются смешные ляпы. Казалось бы — при чем здесь .htaccess, если используется встроенный сервер РНР?


    Если бы я писал подобную статью для новичков, то взял бы за основу знаменитый текст Symfony versus Flat PHP, и уже от него прокладывал мостик к фреймворку. Чтобы была понятна связь — какой элемент фреймворка соответствует нашему файлику корзина.пыхыпы


    Статья явно рассчитана на начинающих. Но без объяснений мы получим из начинающего не программиста, а классического лепилу, который "может написать свой фреймворк!" Ага, так же как он может "написать приложение на Ларавле!" И чуть в сторону от учебника — тут же с вопросом на Тостер, "а как мне посчитать сумму в SQL запросе?"...


  1. ilyaplot
    12.03.2022 00:48

    Apache в 2022? Где строгая типизация? Автор знает про наличие таких методов как PUT, DELETE и остальных? Зачем $request public? И вообще, какой смысл поста, какую мысль хочет донести автор?

    Пост про код из одной строки, но с composer, psalm, codeception, bitbucket pipelines или github actions и docker будет гораздо полезнее, чем вот эта спорная никому не нужная структура, вызывающая массу вопросов.


    1. FanatPHP
      12.03.2022 10:54

      Апача тут нет, тут используется встроенный сервер :)


      В целом я бы сказал что часть этих придирок не по делу. Типизацию — да, надо уже ставить просто на автомате. Но вот про put c delete тут явно не к месту. Понятно что класс реквест не полный, в нем к примеру не разбирается инпут, который куда важнее, чем пут :)
      Но это как раз нормально, чтобы не загромождать код и текст. Код из одной строки и псалм вообще не к месту, у автора про другое.


      Я это к тому, что статья хоть и заслуживает критики, но критики адекватной, а не "почему про докер не написал?"