Опции и настройки в PHP-проекте
20-04-2024Время чтения ~ 14 мин.Albireo Framework / CMS 771
![](https://maxsite.org/website/uploads/pages/876-options.webp)
Любой проект должен иметь возможность настраиваться под задачи пользователя. В идеале пользователь должен иметь возможность поменять абсолютно любую вариативную часть системы. Самое простое — это что-то вроде названия сайта и более сложное, когда настраивается модульный вывод главной страницы. Разработчики к этому вопросу подходят по разному. Одни стараются снабдить настройками как можно больше модулей своего проекта, другие же всё жёстко фиксируют, не позволяя пользователю что-то менять.
Обычно считается, что чем больше возможностей по настройке системы у пользователя, тем лучше. Но здесь должен быть баланс, когда количество опций может оказаться слишком большим, что в итоге сделает их управление слишком сложным.
Принципиально есть два уровня настроек. Первый — это некий глобальный уровень, который обычно и называется опциями. Для их получения используется что-то вроде getOptions('опция', 'дефолт')
. Такие опции хранятся в базе данных или в файлах конфигурации. Например название сайта, часовой пояс, используемый шаблон и т.п. То есть это те данные, которые не зависят от используемой страницы.
Если расширить опции на группы, то в них можно хранить данные виджетов, сайдбаров, модулей — то есть группировка позволяет выделить только те опции, которые имеют отношение к какой-то части системы.
Архитектурно любые такие опции представляют собой обычный PHP-массив. Таким образом получение любой опции из этого массива сводится к простой строке:
$value = $options['ключ'] ?? 'дефаулт';
Второй уровень настроек — это те опции, которые имеют отношение к непосредственной странице записи. Очень часто такие опции называют meta, показывая, что у страницы есть расширяемая meta-таблица. В этой таблице всегда есть ключ, который указывает на идентификатор записи. Соответственно, при получении данных из основной таблицы записи (контент, заголовок и т.п.) автоматом подтягиваются данные из meta-таблицы.
Фактически это тот же самый PHP-массив, где хранятся все мета-данные страницы.
В теории всё это несложно, но на практике есть большая проблема в том, как именно пользователь определяет все эти настройки.
Опять же, есть два способа поменять опции: прямо в конфигурационном файле, либо через админ-панель.
Конфигурационный файл самый простой вариант: открыл файл, внес правку, сохранил.
С админ-панелью всё намного сложней.
Для того, чтобы внести опцию, следует этой самой админ-панели где-то указать какие именно опции нужно отобразить на странице. Причём опции могут быть числом, строкой, выбранным элементом (SELECT/RADIO), многострочным элементом, булевским (это уже чекбокс).
Если пользователь захочет вести новую опцию, то вначале ему придётся как-то отдельно её описать, а уже потом он сможет её увидеть и изменить.
С точки зрения компетентности конечного пользователя сайта — такая задача ему не под силу. Поэтому разработчики систем придумывают способы упростить этот процесс. Но в любом случае опции через админку — это всегда ограничение возможностей системы.
При проектировании Albireo CMS я задался целью сделать так, чтобы у пользователя была возможность использовать любые опции и мета-данные. Поскольку это система flat file, то все опции хранятся в виде файлов.
Есть каталог config
, который хранит файлы с конфигурацией. Файлов может быть сколько угодно, но каждый из них возвращает массив. Поэтому получение любого ключа (читай опции) или всего массива сводится к простому getConfigFile('файл', 'ключ')
.
Но при этом есть некий «главный» config.php
, поэтому к нему проще обращаться через getConfig('ключ')
. То есть всё довольно просто.
Возникает вопрос — а как менять опции, ведь это работа с файлом? Да, здесь всё также просто. Есть админ-панель, которая позволяет выбрать файл, а потом его отредактировать через встроенный редактор.
То есть фактически такой подход избавляет от необходимости создавать сложный механизм работы с опциями в админке. Может показаться, что это «некошерно», поскольку владелец сайта получает прямой доступ к php-файлу на сервере. Но в рамках истинно гибкой CMS по другому и не получится. Работать с файлом совсем несложно, при этом мы всегда держим в уме тот факт, что если пользователь боится что-то сломать, то он всегда обратится к своему вебмастеру. То есть компетентность пользователя сайта всегда будет достаточной, чтобы не ограничивать его в возможностях.
Есть ряд систем, которые не позволяют работать с php-файлами и вместо этого предлагают различные суррогаты. Самым популярным является YAML. Когда-то я работал с этим форматом, но отказался, по нескольким причинам. Первая — его техническое обслуживание очень дорогое. Библиотеки для работы с YAML слишком ресурсоёмкие, поскольку разметка файла может быть сложной. Другая причина — формат YAML пытается повторить то ли XML, то ли JSON. Его формат просто не позволяет явно понять нужно ли мне число, строка или булево значение. В этом плане обычный PHP проще и ясней.
Остальные разметки: JSON, INI, TOML просто нет смысла рассматривать, поскольку они однозначно сложнее PHP-массива.
Так что если мы считаем, что компетентность пользователя сайта выше интеллекта обезьяны, то нет никакого смысла ему запрещать работать с PHP. Формат массива PHP ничем не уступает любым другим текстовым разметкам, но при этом имеет ещё одно неоспоримое преимущество — это исполняемый файл, а значит в нём можно выполнять подпрограммы для формирования динамических опций. Возможно, что на этапе проектирования системы будет казаться, что этого не потребуется, но, как показывает жизненный опыт, хрен там!
Теперь рассмотрим meta-опции, то есть те, которые привязаны к самой записи.
Когда я сделал LPF (Landing Pages Framework), это было 10 лет назад, там была схема, когда страница хранилась в отдельном подкаталоге и там был специальный php-файл, который и описывал все опции данной страницы. То есть идея была в том, чтобы разделить страницу на два файла — непосредственно контент и её опции. Многие современные Flat File CMS также подхватили эту идею и до сих пор их пользователи мудохаются с этими файлами.
В Albireo я отказался от этой схемы, потому что фактически нет разницы прочитать два раза один и тот же файл или два отдельных файла. В Albireo служебная часть размещается в PHP-комментарии, поэтому когда вначале получается только она, то файл считывается построчно и быстро. А когда уже выводится сам контент, то подключается обычным require
, а служебная часть игнорируется естественным для PHP способом (поскольку это комментарий).
<?php /** служебная часть **/ ?> произвольный контент
Удобство в том, что всё в одном файле и не нужно тыркаться с дополнительным.
Каждое поле в служебной части задаётся в привычном формате, напоминающим YAML. Но в отличие от YAML здесь есть только обычные строки и ничего больше.
title: Название страницы slug: test date: 08-04-2024 23:35 # date-edit: 08-04-2024 23:35 head-meta[robots]: noindex sitemap: - stat: + compress: + comments: - user-level: admin layout: sidebar.php
Сами поля могут быть произвольными, у них единые правила. Часть полей прописана на уровне системы. Изначально я думал, что можно сделать их настраиваемыми, но потом понял, что небольшая унификация будет иметь больше практической пользы.
При проектировании этих настроек я исходил из удобства пользования. Например значение поля может быть +
или -
(вкл/выкл), которые намного лучше обычного true/false
или 1/0
. Группировки полей может быть произвольной. Например если используются квадратные скобки «[]», то это позволит вывести эти опции по шаблону с «key/val».
Сама же группировка однотипных опций может быть с помощью «-», либо через точку «.».
use.alpine: + use.highlight: + use.clipboard: + css.theme: indigo.css css.fonts.lazy[]: lora.css css.fonts.lazy[]: exo2.css
При создании новой записи/файла, пользователю не нужно вводить все эти поля, поскольку можно сделать файл-шаблон, который будет содержать полный каркас.
title: Астрономия description: [announce] type: blog date: 2020-09-24 10:28:20 category: astro layout: sidebar.php comments: + image-large: [UPLOADS_URL]3.webp announce: анонс
Сами поля могут меняться динамически. Конечно же в них можно использовать обычный php-код. Также в них могут быть автозамены на некоторые PHP-константы, например UPLOADS_URL заменится на http-адрес к загрузкам.
image-large: [UPLOADS_URL]gal/landscape/1@1200.webp image-small: <?= hThumb('//gal/landscape/1@1200.webp', 900, urlOnly: true) ?> ogp[og:image]: [image-large]
Поля могут ссылаться на другие поля. Это достаточно частая потребность, чтобы не дублировать текст. Ну и вишенка на торте — это условные поля, когда поле может содержать значение другого поля, но если его нет или одно пустое, то использовать третье поле, либо какой-то текст.
filed1: my1 filed2: my2 test1: @if [filed1] @else 'hello!' test2: @if [filed1] @else [field2]
Сработает если поля filed1 не будет, или оно будет пустым.
В конфигурационном файле можно задать поля по умолчанию для всех страниц. То есть их можно не указывать явно. Но если нужно переопределить, то указываем что нужно уже в самой записи. С помощью условий можно задать опции и их дефолтное значение и это избавит пользователя от необходимости переписывать опцию на странице. Например так задана опция изображения для Open Graph:
ogp[og:image]: @if [image-large] @else '[UPLOADS_URL]ogp-default.webp'
Если на странице не будет поля image-large, то будет использовано дефолтное изображение.
Смысл такого подхода в том, чтобы дать пользователю свободу в определении полей. Фактически все эти данные доступны в виде обычного php-массива. Если пользователю нужно добавить новое поле, то он просто его добавляет. Дальше он может получить к нему доступ как к элементу массива:
$test1 = getVal('pageData')['test1'] ?? '';
Спрашивается, а зачем вообще такая гибкость и возможность? Приведу пару примеров.
Например я хочу использовать в шаблоне Bootstrap Icons. Я скачал его и могу прописать его постоянное использование, но понимаю, что не на всех страницах он нужен. Таким образом подключение будет только на тех страницах, где он явно указан:
use.bootstrap-icons: +
В файле шаблона, в котором происходит непосредственное подключение css-файла я учитываю эту опцию:
if (checkStr(getPageData('use.bootstrap-icons', false)) === true) echo '<link rel="stylesheet" href="' . TEMPLATE_URL . 'vendor/bootstrap-icons/bootstrap-icons.css">';
Больше никаких действий не потребовалось. Если бы опции нужно было бы настраивать через админку, то как минимум нужно было бы написать приличный код, регистрирующий и обслуживающий эту опцию.
Второй пример — это вывод записей в произвольной группировке. Традиционно в сайтах применяется деление на static, post, blog, draft и т.п. Это отдельные enum-поля в таблице записи. Если бы мы хотели добавить тип записи favorite, то потребовались бы ещё и манипуляции с базой или ядром системы. Потому что выборку записей можно сделать только через SQL-запрос.
Поскольку в Albireo CMS поле может быть произвольным, то при выборке вообще всё равно, какое указывать.
$files1 = getPagesField('type', 'favorite'); // записи с type: favorite $files1 = sortArray($files1, 'date', 'date-desc'); // новые вверху $files1 = array_slice($files1, 0, 7, true); // обрезать
То есть у пользователя есть возможность придумать любое поле для любой группировки записей. Это может быть что угодно. например можно выбрать записи, которые используют use.bootstrap-icons
$files1 = getPagesField('use.bootstrap-icons', '1');
Или те, у которых отмечен уровень доступа «admin»:
$files1 = getPagesField('user-level', 'admin');
Если вы работаете с любой CMS, то такой уровень гибкости невозможен. Но здесь это делается элементарно.
При работе с опциями обычно требуется баланс между их количеством и удобством пользования. Разработчику системы кажется, что он предусмотрел все необходимые опции и даже придумал механизм их обслуживания, но всегда найдётся клиент, которому текущих настроек будет недостаточно. Поэтому идеальный вариант — это возможность создавать произвольные опции на уровне пользователя, а их обслуживание и программирование на уровне вебмастера. Тогда разработчику системы не придётся постоянно обновлять свой проект после каждого клиента.
Опции, который определяет пользователь — ключевая возможность для развития его сайта. Если разработчик системы не предлагает хоть какой-то механизм для этого, то система рано или поздно зайдёт в тупик. Например CMS, в которой плохо продуманы шаблоны. Разработчик системы будет исходить из предположения, что клиенту будет достаточно только подредактировать css-файл. Но сегодняшние реалии таковы, что этого недостаточно — часто клиенту нужно не просто отредактировать css-код, но и целиком его заменить на другой css-фреймоврк, а также различные js-библиотеки, шрифты и т.п. Если бы разработчик предусмотрел возможность опционально подключать произвольные файлы, то проблему удалось бы решить. Иначе же разработчик системы вынужден сам добавлять файлы клиента и обходить свои же ограничения системы.
Если система основана на базе данных, то для её управления всегда нужна админ-панель. Потому что для работы с базой всегда нужен интерфейс. То что почти все современные CMS основаны на принципах работы с базой, лично я вижу как историческое недоразумение.
Изначально WEB был не приспособлен для создания полноценных приложений. PHP всегда рассматривался как динамический HTML, поэтому не имел и не имеет средств для создания обычных приложений (application). Если взять за основу языки Си, Паскаль, Дельфи, Яву, Пайтон и т.п., то все они работают вне WEB'а. PHP же, наоборот, не имеет ничего из application — на нём невозможно создать нормальное консольное или win-приложение, как например на Пайтоне или Яве. Всё что может PHP — это работать на сервере и обслуживать http-запросы.
Поэтому, когда появилась потребность в управлении контентом сайта, то за основу был взят PHP, не имеющих необходимых средств на борту, но который пытался имитировать работу полноценных языков.
База данных в WEB — это в первую очередь хранилище данных, хотя в любом другом языке — это прежде всего структурированная информация. Для сайта база используется в первую очередь из-за проблем файловых хранилищ — они недостаточно надёжны. База же, хоть и требует отдельной установки, более удобный способ хранения. Но поскольку база имеет структуру, то этот принцип перешёл и в WEB.
Разница довольно существенна. Любой, кто писал программы для баз, знает, что самое важное это продумать все таблицы, и все поля. Учесть ключи, каскадное обновление, связи. А уже после этого проектирования создаётся внешний интерфейс.
Кто не в курсе, расскажу, как в Delphi можно сделать приложения для работы с базой. Запускается специальный мастер, где просто указывается либо существующая база и таблицы, либо создаётся новая база, таблицы и т.п. На следующем этапе мастер предлагает выбрать формат приложения и варианты работы — таблицей или полями (текстовые, селекты и т.п.). В итоге мастер сам формирует рабочий каркас программы, который автоматически строится над структурой базы. При этом программирования — ноль.
Когда-то был такой язык Visual FoxPro, где программа строилась прямо в комплекте с базой данных. Причем это именно визуальное построение и базы и программы. Частенько встречались интересные экземпляры...
Из современных подобных баз наверное стоит отметить Microsoft Access и LibreOffice Base. Там также вначале строится база, а уже потом под неё подстраивается внешний интерфейс.
То есть я хочу сказать, что WEB использует базу, просто как хранилище, а все эти навороты вроде структуры, и вообще SQL — это неотделимый придаток к базе. Но если взять любую CMS на базе, то в ней нет и десятой части того, что называется полноценным программированием баз данных.
Из-за этого подражания, возникла идея в том, что WEB может быть структурирован. XHTML — скривились? Провальная идея, потому что WEB более гибкий и его невозможно загнать в жесткую структуру. Более того, стандарты WEB развиваются. То что сейчас работает с браузере, например нативная фильтрация форм, об этом можно было только мечтать 20 лет назад.
Точно также отмирают и возникают новые модные тенденции. Раньше jQuery был стандартом, сейчас все от него отказываются и рассматривают как аппендикс. CSS, HTML и JavaScript — очень динамичные технологии, поэтому то, что было раньше, сейчас используется уже в другом ракурсе.
Поэтому любой web-сайт будет развиваться и если его структура имеет жесткие ограничения, то это приведёт его к стагнации. База, которую предложил разработчик web-системы никогда не сможет удовлетворить постоянно меняющиеся потребности пользователей. То что на сайте используется база ещё не значит, что её структура будет соответствовать реальным потребностям.
Если следовать парадигме программирования баз данных, то каждое поле на сайте, каждая опция, каждая настройка должна быть отражением поля из базы. Если разработчик не предусмотрел опцию использования иконок Бутстрапа, то у пользователя должен быть механизм, позволяющий добавить недостающее поле в базу.
Проблема на самом деле даже больше, чем кажется. Например 99% CMS имеют таблицу с текстом записи (content), но при этом рядом находятся поля title, slug, date и т.п. Казалось бы всё логично, ведь именно эти данные нужны при выборке. Однако в 99% случаев этих полей будет недостаточно, поэтому используют дополнительную таблицу meta, которая состоит из полей ключ и значение и реферная ссылка на таблицу контента. Таким образом запрос к базе строится как запрос к двум таблицам. И, если подумать, то в таблице контента, дополнительные поля уже лишние — они должны быть в таблице meta. А в таблице контента должно быть только два поля: id и текст записи.
Если вы, как php-разработчик, решите написать свою систему, то подумайте об этом. База — это просто хранилище данных, фактически аля-файл, поэтому рассматривать контент записи следует отдельно от её информации.
Соответственно, при создании или редактировании записи, должны быть доступны все поля из таблицы meta. Если поля нет, то оно может быть создано тут же. То есть фактически нужно повторить работу с опциями как это происходит при работе с файлами: создание, отображение и редактирование. В текстовом файле — это все делается ручками, а с базой нужен дополнительный интерфейс.
Еще один момент в заключении — это опции, которые предоставляют выбор из заданного значения. Например в одной CMS есть поле visibility, которое может иметь значения draft, visible и hidden. Проблема таких полей в том, что пользователь должен помнить весь список значений. С точки проектирования базы — это означает, что нужно сделать либо enum-поле в таблице, либо отдельную таблицу доступных значений.
Однако на практике все решается немного проще. Вместо одной опции лучше использовать три - по количеству возможных вариантов.
draft: + visible: - hidden: -
Такой подход позволяет упростить работу с опцией, и при этом сразу показать логические нестыковки, чем например draft отличается от hidden? Или draft это not hidden? То есть чем проще опция, тем лучше. В идеале, если поле может быть только текстом, то это сильно упростит проектирование интерфейса пользователя. В этом случае можно вообще отказаться от дополнительной операции по описанию опции. Минус только в том, что изчезает удобство выбора полей вкл/выкл - так это мог бы быть select или checkbox, а будет текстовое поле, где нужно будет вручную набрать «+» или «-».
Слава Украине! Смерть рашистам!