dashiwa / own

OWN PHP
2 stars 0 forks source link

Symfony own framework #11

Open dashiwa opened 5 years ago

dashiwa commented 5 years ago

Почему вы хотите создать свой собственный фреймворк?

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

https://symfony.com/doc/current/create_framework/index.html Узнать больше о низкоуровневой архитектуре современных веб-фреймворков в целом и о внутренностях фреймворка полного стека Symfony в частности; Создать структуру, адаптированную к вашим очень специфическим потребностям (сначала убедитесь, что ваши потребности действительно специфичны);

Экспериментировать, создавая основу для развлечения (в подходе «учись и выбрасывай»); Реорганизовать старое / существующее приложение, которое нуждается в хорошей дозе недавних лучших практик веб-разработки;

Чтобы доказать миру, что вы действительно можете создать структуру самостоятельно (... но без особых усилий).

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

И каждый шаг станет поводом узнать больше о некоторых компонентах Symfony.

dashiwa commented 5 years ago

Bootstrapping

// framework/index.php
$name = $_GET['name'];

printf('Hello %s', $name);
dashiwa commented 5 years ago

The HttpFoundation Component

Во-первых, если nameпараметр запроса не определен в строке запроса URL, вы получите предупреждение PHP; так что давайте исправим это:

// framework/index.php
$name = isset($_GET['name']) ? $_GET['name'] : 'World';

printf('Hello %s', $name);

Даже этот простой фрагмент кода PHP уязвим к одной из самых распространенных проблем безопасности в Интернете, XSS (межсайтовый скриптинг). Вот более безопасная версия:

$name = isset($_GET['name']) ? $_GET['name'] : 'World';

header('Content-Type: text/html; charset=utf-8');

printf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'));

Вот предварительный модульный тест PHPUnit для приведенного выше кода:

// framework/test.php
use PHPUnit\Framework\TestCase;

class IndexTest extends TestCase
{
    public function testHello()
    {
        $_GET['name'] = 'Fabien';

        ob_start();
        include 'index.php';
        $content = ob_get_clean();

        $this->assertEquals('Hello Fabien', $content);
    }
}
dashiwa commented 5 years ago

ООП с компонентом HttpFoundation

Написание веб-кода о взаимодействии с HTTP. Итак, фундаментальные принципы нашей структуры должны быть вокруг спецификации HTTP .

Спецификация HTTP описывает, как клиент (например, браузер) взаимодействует с сервером (наше приложение через веб-сервер). Диалог между клиентом и сервером определяется четко определенными сообщениями , запросами и ответами: клиент отправляет запрос на сервер, и на основании этого запроса сервер возвращает ответ .

В PHP запрос представлен глобальными переменными ( $_GET, $_POST, $_FILE, $_COOKIE, $_SESSION...) и ответ порождается функциями ( echo, header, setcookie, ...).

Первый шаг к лучшему коду, вероятно, заключается в использовании объектно-ориентированного подхода; это главная цель компонента Symfony HttpFoundation: замена глобальных переменных и функций PHP по умолчанию на объектно-ориентированный слой.

Чтобы использовать этот компонент, добавьте его в качестве зависимости проекта:

composer require symfony/http-foundation

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

При установке новой зависимости Composer также генерирует vendor/autoload.phpфайл, который позволяет автозагрузке любого класса . Без автозагрузки вам понадобится файл, в котором определен класс, прежде чем вы сможете его использовать. Но благодаря PSR-4 мы можем просто позволить Composer и PHP сделать тяжелую работу за нас.

Теперь, давайте перепишем наше приложение с помощью Requestи на Responseклассы:

// framework/index.php
require_once __DIR__.'/vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();

$name = $request->get('name', 'World');

$response = new Response(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')));

$response->send();

Метод createFromGlobals() создает Request объект , основанный на текущем PHP глобальных переменных.

Метод send() отправляет Response объект обратно клиенту (он сначала выводит HTTP заголовков с последующим содержанием).

Перед send()вызовом мы должны были добавить вызов prepare()метода ( $response->prepare($request);), чтобы убедиться, что наш ответ соответствует спецификации HTTP. Например, если бы мы вызывали страницу с помощью HEADметода, она бы удалила содержимое ответа.

dashiwa commented 5 years ago

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

ЗАМЕТКА Мы явно не установили Content-Typeзаголовок в переписанном коде, так как кодировка объекта Response по умолчанию имеет значение UTF-8.

Благодаря Requestклассу у вас есть вся информация о запросах под рукой благодаря красивому и простому API:

// the URI being requested (e.g. /about) minus any query parameters
$request->getPathInfo();

// retrieve GET and POST variables respectively
$request->query->get('foo');
$request->request->get('bar', 'default value if bar does not exist');

// retrieve SERVER variables
$request->server->get('HTTP_HOST');

// retrieves an instance of UploadedFile identified by foo
$request->files->get('foo');

// retrieve a COOKIE value
$request->cookies->get('PHPSESSID');

// retrieve an HTTP request header, with normalized, lowercase keys
$request->headers->get('host');
$request->headers->get('content-type');

$request->getMethod();    // GET, POST, PUT, DELETE, HEAD
$request->getLanguages(); // an array of languages the client accepts
dashiwa commented 5 years ago

Вы также можете смоделировать запрос:

$request = Request::create('/index.php?name=Fabien');

С помощью Response класса вы можете настроить ответ:

$response = new Response();

$response->setContent('Hello world!');
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/html');

// configure the HTTP cache headers
$response->setMaxAge(10);

Чтобы отладить ответ, приведите его к строке; он вернет HTTP-представление ответа (заголовки и содержимое).

Наконец, что не менее важно, эти классы, как и любой другой класс в коде Symfony, были проверены независимой компанией на предмет проблем безопасности. Быть проектом с открытым исходным кодом также означает, что многие другие разработчики по всему миру прочитали код и уже исправили потенциальные проблемы безопасности. Когда вы в последний раз заказывали профессиональный аудит безопасности для своих самодельных рамок?

Даже такая простая вещь, как получение IP-адреса клиента, может быть небезопасной:

if ($myIp === $_SERVER['REMOTE_ADDR']) {
    // the client is a known one, so give it some more privilege
}
dashiwa commented 5 years ago

Он отлично работает, пока вы не добавите обратный прокси перед рабочими серверами; на этом этапе вам придется изменить свой код, чтобы он работал как на вашей машине разработки (где у вас нет прокси), так и на ваших серверах:

if ($myIp === $_SERVER['HTTP_X_FORWARDED_FOR'] || $myIp === $_SERVER['REMOTE_ADDR']) {
    // the client is a known one, so give it some more privilege
}

Использование Request::getClientIp() метода дало бы вам правильное поведение с первого дня (и это охватило бы случай, когда у вас были цепочки прокси):

$request = Request::createFromGlobals();

if ($myIp === $request->getClientIp()) {
    // the client is a known one, so give it some more privilege
}

И есть дополнительное преимущество: по умолчанию это безопасно . Что это значит? $_SERVER['HTTP_X_FORWARDED_FOR'] Значение нельзя доверять , как это можно манипулировать конечным пользователем , когда нет прокси. Таким образом, если вы используете этот код в производстве без прокси-сервера, вам будет легко злоупотреблять вашей системой. Это не относится к getClientIp() методу, так как вы должны явно доверять своим обратным прокси, вызывая setTrustedProxies():

dashiwa commented 5 years ago
Request::setTrustedProxies(['10.0.0.1']);  if ($myIp === $request->getClientIp()) {     // the client is a known one, so give it some more privilege }
--

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

dashiwa commented 5 years ago

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

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

На самом деле, такие проекты, как Drupal, приняли компонент HttpFoundation; если это работает для них, это, вероятно, будет работать для вас. Не изобретай велосипед.

Я почти забыл поговорить об одном дополнительном преимуществе: использование компонента HttpFoundation является началом лучшей совместимости между всеми фреймворками и приложениями, использующими его (такими как Symfony , Drupal 8 , phpBB 3 , ezPublish 5 , Laravel и т. Д. ).

Эта работа, включая примеры кода, лицензирована под лицензией Creative Commons BY-SA 3.0 .

dashiwa commented 5 years ago

The Front Controller

До сих пор наше приложение было упрощенным, поскольку в нем всего одна страница. Чтобы немного оживить ситуацию, давайте сходим с ума и добавим еще одну страницу, которая прощается:

// framework/bye.php
require_once __DIR__.'/vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();

$response = new Response('Goodbye!');
$response->send();

Как видите, большая часть кода точно такая же, как та, которую мы написали для первой страницы. Давайте извлечем общий код, которым мы можем поделиться между всеми нашими страницами. Совместное использование кода звучит как хороший план по созданию нашей первой «настоящей» платформы!

PHP-способ выполнения рефакторинга, вероятно, заключается в создании включаемого файла:

// framework/init.php
require_once __DIR__.'/vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();
$response = new Response();
dashiwa commented 5 years ago

Давайте посмотрим на это в действии:

// framework/index.php
require_once __DIR__.'/init.php';

$name = $request->get('name', 'World');

$response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')));
$response->send();
dashiwa commented 5 years ago

Мы действительно переместили большую часть общего кода в центральное место, но это не похоже на хорошую абстракцию, не так ли? У нас все еще есть send()метод для всех страниц, наши страницы не похожи на шаблоны, и мы все еще не можем должным образом протестировать этот код.

Более того, добавление новой страницы означает, что нам нужно создать новый скрипт PHP, имя которого предоставляется конечному пользователю через URL ( http://127.0.0.1:4321/bye.php): между именем скрипта PHP и URL клиента существует прямое сопоставление. Это связано с тем, что отправка запроса осуществляется веб-сервером напрямую. Это может быть хорошей идеей перенести эту диспетчеризацию в наш код для большей гибкости. Это может быть достигнуто путем маршрутизации всех клиентских запросов к одному сценарию PHP.

Предоставление отдельного PHP-скрипта конечному пользователю - это шаблон проектирования, называемый « фронт-контроллер ».

Такой скрипт может выглядеть следующим образом:

// framework/front.php
require_once __DIR__.'/vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();
$response = new Response();

$map = [
    '/hello' => __DIR__.'/hello.php',
    '/bye'   => __DIR__.'/bye.php',
];

$path = $request->getPathInfo();
if (isset($map[$path])) {
    require $map[$path];
} else {
    $response->setStatusCode(404);
    $response->setContent('Not Found');
}

$response->send();

А вот, например, новый hello.php скрипт:

dashiwa commented 5 years ago

В front.php сценарии $map связывает пути URL с соответствующими путями сценария PHP.

В качестве бонуса, если клиент запрашивает путь, который не определен в карте URL, мы возвращаем пользовательскую страницу 404; Теперь вы контролируете свой сайт.

Чтобы получить доступ к странице, теперь вы должны использовать front.phpскрипт:

http://127.0.0.1:4321/front.php/hello?name=Fabien
http://127.0.0.1:4321/front.php/bye

/hello и пути /bye к страницам .

dashiwa commented 5 years ago

Большинство веб-серверов, таких как Apache или nginx, могут переписывать входящие URL-адреса и удалять скрипт фронт-контроллера, чтобы ваши пользователи могли печатать http://127.0.0.1:4321/hello?name=Fabien, что выглядит намного лучше.

dashiwa commented 5 years ago

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

СОВЕТ Вам даже не нужно настраивать веб-сервер для тестирования кода. Вместо этого замените $request = Request::createFromGlobals(); вызов чем-то вроде, $request = Request::create('/hello?name=Fabien'); где аргумент - это URL-путь, который вы хотите смоделировать.

dashiwa commented 5 years ago

Теперь, когда веб-сервер всегда имеет доступ к одному и тому же скрипту ( front.php) для всех страниц, мы можем дополнительно защитить код, переместив все остальные файлы PHP за пределы корневого веб-каталога:

example.com
├── composer.json
├── composer.lock
├── src
│   └── pages
│       ├── hello.php
│       └── bye.php
├── vendor
│   └── autoload.php
└── web
    └── front.php
dashiwa commented 5 years ago

Теперь настройте корневой каталог вашего веб-сервера так, чтобы он указывал на web/все остальные файлы, и клиент больше не будет к ним доступ.

Чтобы проверить свои изменения в браузере ( http://localhost:4321/hello?name=Fabien), запустите встроенный сервер PHP:

php -S 127.0.0.1:4321 -t web/ web/front.php

Чтобы эта новая структура работала, вам нужно настроить некоторые пути в различных PHP-файлах; изменения оставлены в качестве упражнения для читателя.

Последнее, что повторяется на каждой странице, это призыв к setContent(). Мы можем преобразовать все страницы в «шаблоны», просто отображая содержимое и вызывая setContent()непосредственно из скрипта фронт-контроллера:

// example.com/web/front.php

// ...

$path = $request->getPathInfo();
if (isset($map[$path])) {
    ob_start();
    include $map[$path];
    $response->setContent(ob_get_clean());
} else {
    $response->setStatusCode(404);
    $response->setContent('Not Found');
}

// ...
dashiwa commented 5 years ago

И hello.phpскрипт теперь можно преобразовать в шаблон:

<!-- example.com/src/pages/hello.php -->
<?php $name = $request->get('name', 'World') ?>

Hello <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>

У нас есть первая версия нашего фреймворка:

// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();
$response = new Response();

$map = [
    '/hello' => __DIR__.'/../src/pages/hello.php',
    '/bye'   => __DIR__.'/../src/pages/bye.php',
];

$path = $request->getPathInfo();
if (isset($map[$path])) {
    ob_start();
    include $map[$path];
    $response->setContent(ob_get_clean());
} else {
    $response->setStatusCode(404);
    $response->setContent('Not Found');
}

$response->send();

Добавление новой страницы состоит из двух этапов: добавьте запись на карту и создайте шаблон PHP в src/pages/. Из шаблона получите данные запроса через $requestпеременную и настройте заголовки ответа через $response переменную.

ЗАМЕТКА Если вы решите остановиться на этом, вы, вероятно, можете улучшить свою инфраструктуру, извлекая карту URL в файл конфигурации.