NoName Framework или Как я велосипед изобретал

в 17:38, , рубрики: framework, php, Веб-разработка, процедурное программирование, метки: , ,

NoName Framework или Как я велосипед изобретал
Привет тебе, читатель!

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

Небольшое предисловие

На данный момент я учусь в подмосковном колледже на третьем курсе по специальности «Программное обеспечение вычислительной техники и автоматизированных систем». На втором семестре третьего курса нам сказали что-то вроде «Теперь вы готовы», и у нас появился новый предмет — Программное обеспечение компьютерных сетей, или, другими словами, нас начали учить HTML'ю. Немного ранее один из моих однокурсников начал изучать HTML, CSS и PHP и добился в этом некоторых успехов — сделал свой интернет-магазин. Как-то я попросил его показать мне исходники, и был не особо удивлен — если не считать некоторое количество инклудов, весь PHP состоял в расширении файлов (корзина и все прочие примочки, составлявшие «магазинную часть» были на Ecommtools тчк com).

Увидев такое, я просто не мог промолчать и начал ему что-то втирать про MVC, про базу и все такое прочее. Естественно, он почти ничего не понял из моих слов (я, как сами видите, объясняю не очень хорошо). Но буквально через пару недель подвернулся вышеупомянутый предмет. Преподаватель по нему вел у нас раньше, и я без особых проблем договорился с ним о том, что мы с однокурсником заниматься будем самостоятельно. В итоге, за энное количество пар, сайт был переписан с нуля моими собственными руками, а однокурсник, кроме нового сайта, получил внушительный багаж знаний (я надеюсь полезных).

Исходники я скинул себе (работали мы на его ноуте) и дома ковырял дальше, добавлял функционал, рефакторил, переделывал кое-что заново. В итоге у меня получилось некое подобие фреймворка, на котором вполне успешно работает несколько проектов. Теперь я решил открыть свое творение народу, дабы тот решил — забрасывать мне веб-программирование или развивать свое детище дальше. Да и мало ли, вдруг кому пригодится.

Особенности фреймворка

1. Малый вес. Ну очень малый. В распакованном виде он занимает около 34 килобайт (Если удалить стандартный текст приветствия и картинку-логотип)
2. Отсутствие ООП. Не знаю, преимущество это или наоборот минус, но по идее ООП-приложения съедают гораздо больше ресурсов, чем приложения, написанные в процедурном стиле. (Какая могла идти речь об объектах, когда человек, которому в первую очередь это демонстрировалось, в основах еще не до конца разобрался)
3. MVC. Ну, тут я думаю объяснять ничего не надо.
4. Роутинг. Маршрутизация (на мой взгляд) очень простая в использовании, но при этом довольно гибкая в настройке, хотя и требует доработки. Маршрутизация по-умолчанию — «Controller/Action/Param-1/Value-1/Param-2/Value-2».
5. JSON и XML. Для того, что бы получить ответ в одном из этих форматов, надо лишь дописать в конце адреса ".json" или ".xml" соответственно. То есть, если по адресу "/goods/view/id/3" вы получаете информацию о товаре, то по адресу "/goods/view/id/3.json" вы можете получить ее в формате JSON.
6. Функционал. Из коробки функционала немного, так как коммьюнити составляет всего 1 человек (я). Пока что есть плохонький скрипт рассылки мыла и вполне рабочая авторизация пользователей с использованием сессий.
7. Бэктрейс. Присутствует. Отключается, если приложение не в debug-режиме.
8. БД. Поддерживается только MySQL. Это минус, но на данный момент не критичный.
9. ORM. Ее быть тут не может, так как ООП отсутствует во фреймворке в принципе. Но кой-какая обвязочка над БД есть.
10. ACL. Отсутствует на данный момент, но в планах по развитию фреймворка (а развиваться он будет только в случае получения хотя-бы нескольких положительных отзывов) стоит на первом месте.
11. DEBUG-режим. Присутствует, влияет на вывод некоторых ошибок, на вывод бэктрейса, на используемые конфиги.

Структура фреймворка

Схема

| - app/
    | - config/
        | - debug/
            | - *.php
        | - *.php
    | - controllers/
        | - */
            | - *Action.php
    | - models/
        | - *.php
    | - views/
        | - custom/
            | - *.phtml
        | - layout/
            | - default.phtml
        | - scripts/
            | - */
                | - *.phtml
| - library/
    | - functions/
        | - default.php
        | - *.php
    | - init/
        | - configs.php
        | - init.php
        | - predispatch.php
    | - models/
        | - basemodel.php
    | - router/
        | - router.php
    | - include.php
| - files/
    | - *
| - index.php
| - .htaccess

Рассмотрим все по порядку

Начнем, пожалуй, с самого низа, то есть с файла .htaccess. Вот его содержимое:

RewriteEngine on
RewriteCond %{REQUEST_URI} !/files
RewriteRule .*$ index.php [L]

AddDefaultCharset UTF-8

То есть любой запрос, кроме начинающегося на "/files" будет перенаправлен на index.php. На самом деле это не единственный и далеко не лучший вариант реврайта, но я уже привык с ним работать и решил пока что не менять. Ну и кодировка по умолчанию: UTF-8. Больше ничего интересного мы тут не увидим.

Перейдем к index.php. Вот его содержимое:

// Включаем или отключаем дебаг (влияет на бэктрейс и загрузку конфигов)
define('APP_DEBUG', TRUE);
// Определяются папки приложения
define('APP_PATH', dirname(__FILE__) . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR);
define('LIBRARY_PATH', dirname(__FILE__) . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR);
// Подключаются все файлы, необходимые для работы фреймворка
require_once(LIBRARY_PATH . 'include.php');
// Запускаем экшн
startAction();
// Загружаем лайаут
loadLayout($_response['layout']);

Тут, я думаю, объяснять ничего не требуется, так как все действия прокомментированы.

Папка files. Как вы возможно помните, только к этой папке имеется доступ «напрямую», то есть, минуя index.php. Соответственно именно в нее складываются такие ресурсы, как таблицы стилей, JavaScript-файлы, изображения и так далее.

Далее рассмотрим папку app. Это, как ясно из названия, папка самого приложения. Она содержит конфигурацию приложения (папка config), контроллеры (папка controllers) и экшны этих контроллеров, модели (папка models) и представления (папка views).

Контроллер представляет собой папку, содержащую в себе PHP-файлы — экшны. Имя папки должно быть в camelCase. Имя файла-экшна должно быть также в camelCase, но с постфиксом «Action» в конце.
То есть, контроллер с названием «goods» и действиями «index», «show», «new», «edit» и «delete» в файловой системе выглядел бы так:

| - controllers/
  | - goods/
    | - indexAction.php
    | - showAction.php
    | - newAction.php
    | - editAction.php
    | - deleteAction.php

Опционально, любой контроллер может содержать файл _init.php. Содержимое этого файла будет выполняться перед запуском любого экшна в этом контроллере.

Теперь о моделях (папка «models»). В моем понимании, модель должна возвращать уже готовые к использованию данные, которые уже не придется дополнительно обрабатывать в контроллере. То есть, например, нужно вывести в блок 5 последних новостей. Можно составить запрос прямо из контроллера, но в моем понимании правильнее было бы свести код к такому виду:

loadModel('news');
$_view['last_news'] = getLastNews(5);

Тут метод getLastNews() объявляется в модели «news» и возвращает массив данных, которые осталось просто оформить и вывести в представлении.
Сама по себе модель представляет собой набор методов, объединенных в один файл.

Перейдем к представлениям — папке «views». Она содержит 3 поддиректории: custom, layout, scripts. Все представления имеют расширение .phtml.
Папка «layout» хранит в себе все лайауты приложения. Тут объяснять особо нечего. Для вывода контента в лайаут используется метод content().
В папке «scripts» хранятся представления для экшнов. Структура такая: scripts/<имяКонтроллера>/<имяЭкшна>.phtml.
Ну а в папке «custom» лежат все остальные представления, которые не подходят для первых двух папок. Например, тут могут находиться блоки сайдбара, вынесенный отдельно хэдер и так далее.

Ну и наконец, папка «config». Она содержит поддиректорию debug, куда конфиги могут быть продублированы. Если константа APP_DEBUG — истина, то приложение попытается найти запрошенный конфиг в папке config/debug, и только если там конфига не окажется, оно подключит его из папки config. Если же приложение не в дебаг режиме, папка debug будет проигнорирована.
Все конфиги — PHP-файлы возвращающие массив, то есть содержимое их примерно такое:

return array(
    // . . . 
);

Папка «library». По сути это и есть сам фреймворк., поэтому подробно рассматривать я ее не буду (стыдно). Затрону лишь несколько деталей.
Папка «functions». Если вам надо расширить функционал фреймворка, то вам сюда. PHP-файлы из этой папки подгружаются при помощи метода loadFunction($fnc). Автолоада пока что нет, но в ближайшем будущем будет. Отдельно стоит обратить внимание на файл «default.php». Он содержит весь основной функционал фреймворка и подключается при старте приложения.
В папке «init» нас интересует файл «init.php». Он выполняется каждый раз при старте приложения и динамическую настройку следует выполнять именно там.
В папке «models» обратите внимание на файл «basemodel.php». Это и есть та самая обвязка для MySQL, про которую я писал выше.

Работа с фреймворком

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

Создание и настройка проекта

Скачиваем фреймворк по одной из ссылок: yadi.sk/d/ZAnE6Yq_4kgBe troloload.ru/f/5979_nonameframework.zip dl.dropboxusercontent.com/u/85783372/NoNameFramework.zip

Распаковываем фреймворк в папку сайта. Наше приложение будет использовать базу данных, поэтому сразу создаем ее удобным вам способом.
Идем в app/config и открываем файл mysql.php. Заменяем значения по умолчанию на свои. У меня получилось так:

// mysql.php
return array(
    'dbHost' => 'localhost',
    'dbUser' => 'root',
    'dbPass' => '',
    'dbName' => 'test',
);

Далее открываем library/init/init.php и проверяем, что бы строчка initDb(); была раскомментирована.
На этом начальную настройку можно считать завершенной.

Создание CRUD-приложения

В первую очередь нам понадобится таблица в базе данных. Пусть она называется books и состоит из столбцов id, title, author, comment, mark. Для этого выполняем этот запрос к созданной нами базе:

CREATE TABLE `books` (
	`id` INT(10) NOT NULL AUTO_INCREMENT,
	`title` VARCHAR(80) NOT NULL,
	`author` VARCHAR(40) NOT NULL,
	`comment` TEXT NOT NULL,
	`mark` INT NOT NULL,
	PRIMARY KEY (`id`)
)
COLLATE='utf8_general_ci'
ENGINE=MyISAM;

Так же нам понадобится модель. Для этого в папке models создаем файл books.php.

Создадим контроллер books. Для этого в папке app/controllers создаем папку books.

CRUD — аббревиатура от Create Read Update Delete (Создание, Чтение, Обновление, Удаление). Именно эти действия наш контроллер и будет производить. Создадим для каждого из этих действий соответствующий экшн. Имена этих экшнов пусть будут new, view, edit и delete. Кроме того нам понадобится экшн для вывода списка всех книг. Его назовем «index». Для этого создаем пять файлов в папке books: indexAction.php, newAction.php, viewAction.php, editAction.php и deleteAction.php.

Некоторым из экшнов нужны файлы представления (view). Создавать мы их будем по мере необходимости. Начнем с экшна new. Для этого в папке views/scripts создаем подпапку books (по имени контроллера) а в ней файл new.phtml (по имени экшна). Временно вставим в него следующий текст: <b>books/new</b>. Теперь запустим ваш веб-сервер и перейдем по адресу «http://<АДРЕС ВАШЕГО САЙТА>/books/new». Там мы должны увидеть текст books/new.

Вернемся к нашему экшну. Откроем файл newAction.php и добавим туда следующий код:

<?php

// Подключаем модель models/books.php
loadModel('books');

// Проверяем, пришли ли данные методом POST
<?php

// Подключаем модель models/books.php
loadModel('books');

// Проверяем, пришли ли данные методом POST
if(isPost()){
    // Если пришли, то заносим их в переменную $post
    $post = getPost();
    // Пытаемся добавить книгу в базу
    if(newBook($post)){
        // Если получилось - перенаправляем на страницу со списком книг
        redirect('books');
    } else {
        // Если не получилось - передаем введенные данные в представление
        $_view['form'] = $post;
    }
}

Обратите внимание на переменную $_view. Именно с помощью нее контроллер общается с представлением. Все значения, переданные в нее, будут доступны и из представлений, и из лайаута.
Кроме того, нам интересна функция newBook(). На самом деле этой функции еще не существует. Давайте создадим ее. Для этого открываем нашу модель (books.php) и вставляем туда следующий код:

<?php

function newBook($array) {
    // Пытаемся вставить строку в таблицу 'books' и возвращаем результат операции
    return insertRow('books', $array);
}

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

<form method="post">
    <label>Название книги: </label>
    <br />
    <input type="text" name="title" value="<?=$_view['form']['title'] ?>" />
    <br />
    <label>Автор: </label>
    <br />
    <input type="text" name="author" value="<?=$_view['form']['author'] ?>" />
    <br />
    <label>Комментарий к книге: </label>
    <br />
    <textarea name="comment"><?=$_view['form']['comment'] ?></textarea>
    <br />
    <label>Ваша оценка книги (от 1 до 10): </label>
    <br />
    <input type="text" name="mark" value="<?=$_view['form']['mark'] ?>" />
    <br />
    <button type="submit">Сохранить</button>
</form>

Теперь можно обновить страницу в браузере и попробовать добавить данные через форму. Данные добавляются, но после редиректа появляется сообщение об отсутствующем представлении. Давайте создадим его (файл будет называться index.phtml).

Теперь переходим в экшн index. Вставляем туда код:

<?php
// Подключаем модель
loadModel('books');
// Получаем данные о всех книгах и передаем их в представление
$_view['books'] = getAllBooks();

Как вы видите, мы опять использовали несуществующую функцию getAllBooks(). Добавляем в нашу модель вот эти строчки:

function getAllBooks() {
    // Возвращаем все строчки, которые есть в таблице
    return fetchAll('books');
}

Переходим к представлению и вставляем туда это:

<?php if($_view['books']) foreach ($_view['books'] as $v) { ?>
#<?=$v['id'] ?>: <a href="<?=baseUrl('books/view/id/'.$v['id']) ?>"><?=$v['title'] ?></a><br />
<?php } ?>

Тут мы сталкиваемся с новой для нас функцией baseUrl(). Она нужна для создания корректных ссылок. В данном случае можно обойтись и без нее, сделав ссылку от корня сайта, но она избавляет от лишней мороки, особенно тех, у кого сайт расположен по адресу вроде localhost/framework/test.

На этом с этим экшном можно закончить. Переходим к следующему в логической цепочке экшну — view. Сразу приведу здесь весь код, так как по аналогии все понятно.

viewAction.php:

<?php
loadModel('books');
// Передаем в функцию getBook() параметр, полученный из адресной строки
 $_view['book'] = getBook(getParam('id'));

Дополнение для модели:

function getBook($id) {
    // Ищем одну-единственную строку с нужным нам айди
    return fetchRow('books', 'id = ' . $id);
}

Представление (не забудьте его создать):

<strong>Название: </strong>
<?=$_view['book']['title'] ?><br />
<strong>Автор: </strong>
<?=$_view['book']['author'] ?><br />
<strong>Комментарий: </strong><br />
<?=$_view['book']['comment'] ?><br />
<strong>Оценка: </strong>
<?=$_view['book']['mark'] ?> из 10<br />
<a href="<?=baseUrl('books/edit/id/'.$_view['book']['id']) ?>">Изменить</a><br />
<a href="<?=baseUrl('books/delete/id/'.$_view['book']['id']) ?>">Удалить</a><br />

Теперь editAction

editAction.php:

loadModel('books');
// Заполняем форму данными из базы
$_view['form'] = getBook(getParam('id'));
if(isPost()){
    $post = getPost();
    // Пытаемся изменить значения
    if(updateBook(getParam('id'), $post)){
        // Если получилось - перенаправляем на страницу с отредактированной книгой
        redirect('books/view/id/'.  getParam('id'));
    } else {
        // Если не получилось - передаем введенные данные в представление
        $_view['form'] = $post;
    }
}
// С помощью этой строчки мы будем рендерить представление
// не edit.phtml, а new.phtml
setResponse('action', 'new');

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

Добавляем функцию в модель:

function updateBook($id, $values) {
    // Обновляем данные в строке с айди равном $id
    return updateRow('books', 'id = ' . $id, $values);
}

И наконец, последний экшн — delete.

deleteAction.php:

loadModel('books');

deleteBook(getParam('id'));
redirect('books');

Функция для модели:

function deleteBook($id) {
    // Удаляем строку с книгой
    return deleteRow('books', 'id = ' . $id);
}

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

Теперь можно немного улучшить получившийся код. Как вы могли заметить, каждый раз, в начале экшна мы выполняли одну и ту же операцию — loadModel('books'). Давайте вынесем ее отдельно. Для этого в папке нашего контроллера создадим файл _init.php и поместим эту строчку туда, а из контроллеров удалим. Потыкайте на ссылки и убедитесь в работоспособности проекта.

Теперь сделаем ссылки на создание новой записи и на список всех записей. Для этого откроем файл app/views/layout/default.phtml. Процедура content() выводит содержимое представления в лайаут. Добавьте прямо над ней следующие строчки кода:

<a href="<?=baseUrl('books') ?>">Список книг</a><br />
<a href="<?=baseUrl('books/new') ?>">Добавить книгу</a><br />

Теперь ссылки на список и форму добавления будет на всех страницах приложения.

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

Давайте определимся, как в идеале должен выглядеть адрес каждого экшна. Я предлагаю такой вариант:

'books' => '',
'books/new' -> 'book/new',
'books/view/id/{id}' -> 'book/{id}',
'books/edit/id/{id}' -> 'book/{id}/edit',
'books/delete/id/{id}' -> 'book/{id}/delete',

Для того, что бы это заработало? открываем файл config/router.php и приводим массив к следующему виду:

return array(
    # Это - роут индексной страницы
    ''                     => 'books',
    # Остальные роуты
    'book/new'             => 'books/new',
    'book/{id|int}'        => 'books/view/id/{id}',
    'book/{id|int}/edit'   => 'books/edit/id/{id}',
    'book/{id|int}/delete' => 'books/delete/id/{id}',
);

Теперь по адресу book/1 будет открываться информация о книге с айди 1. Остальное по аналогии.

Чуть подробнее о синтаксе маршрутов

Ключом ассоциативного массива является шаблон маршрута, а значением — реальный адрес экшна со всеми параметрами. Динамические части заключаются в фигурные скобки. Имейте в виду, что каждый блок маршрута может либо полностью статическим, либо полностью динамическим, то есть такая запись — 'user/{id}' => 'users/profile/id/{id}' — будет верной, а запись 'id{id}' => 'users/profile/id/{id}' — неверной, то есть на данный момент ссылок вроде yoursite.com/id33523 добиться не получится, хотя я собираюсь исправить это в ближайшее время.

Маршруты имеют фильтрацию по типу данных (строка — string, str; целое число — int, integer; дробное число — float, real) и максимальной длине передаваемого значения. Для добавления фильтра, после имени параметра укажите фильтры через вертикальную черту. Примеры:

    # Просто роут
    '{id|int}'                       => 'index/index/id/{id}',
    # Роуты для тестов
    'test/{test|3}'                  => 'index/index/result/success1/param/{test}',
    'test/{test|integer}'            => 'index/index/result/success2/param/{test}',
    'test/{test|float}'              => 'index/index/result/success2/param/{test}',
    'test/{test|str|5}'              => 'index/index/result/success4/param/{test}',
    'test/{test|string}'             => 'index/index/result/success5/param/{test}',
    # Айди после контроллера запускает экшн 'view'
    '{controller}/{id|int}'          => '{controller}/view/id/{id}',
    # Все айдишники идут сразу после экшна
    '{controller}/{action}/{id|int}' => '{controller}/{action}/id/{id}',

Заключение

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

Ссылки на скачивание

Яндекс.Диск: yadi.sk/d/ZAnE6Yq_4kgBe
Dropbox: dl.dropboxusercontent.com/u/85783372/NoNameFramework.zip
Troloload: troloload.ru/f/5979_nonameframework.zip

БОЛЬШАЯ ПРОСЬБА! Тыкайте в ошибки носом и не стесняйтесь указывать на слабые места. Даже высказывания типа «Бросай программирование — не твое это» будут приняты с благодарностью, если будут подкреплены аргументами. Заранее спасибо!

Автор: SazereS

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js