Мне часто приходилось сталкиваться с нодовыми интерфейсами в программах. Начиная с музыкальных модульных приложений, заканчивая пакетами для создания трехмерной графики.
Идея графического управления логикой программы мне всегда казалась очень элегантным, а в некоторых случаях, единственным удачным решением. Позже, когда помимо музыки и видео я увлекся программированием (в основном PHP, так уж сложилось), мне захотелось попробовать, пусть даже в качестве эксперимента, создать графическую оболочку для выполнения тех нехитрых задач, с которыми я сталкивался в своей работе.
Мое знакомство с нодовым интерфейсом (или графическим программированием) началось с программы Plogue Bidule. Это приложение относится к семейству модульных программ для управления звуком в реальном времени. В свое время эта программа помогала мне решать достаточно нетривиальные задачи, касающиеся обработки звука и синхронизации сценического оборудования на живых выступлениях нескольких моих музыкальных проектов.
Интерфейс программы Plogue Bidule выглядит следующим образом:
В основе работы — создание логических цепочек. Как мне кажется, такой подход вполне естественно вписывается в работу со звуком, хотя кому-то может показаться сложным с первого взгляда. Звук в программе — это непрерывный поток, который появляется из источника (входящий канал звуковой карты, аудио файл или генератор сигнала), и после определенной обработки чаще всего отправляется на выход звуковой карты или на запись в файл.
Главные управляющие конструкции интерфейса — ноды (или модули, блоки, узлы и т.д.), которые по сути являются функциями. По аналогии с музыкальным оборудованием — это приборы, соединенные между собой проводами.
Plogue Bidule включает в себя много интересных возможностей, которые не ограничены только работой со звуком, но для данной статьи этого описания будет достаточно.
Мне повезло впервые познакомиться с нодовым интерфейсом именно в музыкальной программе, поскольку поток звука, на мой взгляд, отличная аналогия для потока выполнения процедурной программы.
Нодовые редакторы широко используются в графических пакетах для создания 3D графики. В таких программах «черные ящики» описывают сложнейшие структуры и функции, а интерфейс позволяет сохранить свободу и гибкость в построении логики выполнения. Самая интересная, на мой взгляд, реализация нодовой системы в программе Houdini.
Так а что же с веб-программированием? Поиск похожих систем не дал результатов. Я подумал, что, возможно, в такой технологии просто отсутствует необходимость. И что идея моя родилась скорее от некомпетентности и наивности. Но желание попробовать создать такой инструмент оказалось сильнее моих сомнений.
Так родился проект Mooha.
PHP, MySQL, HTML
Каким же образом можно соединить эти три технологии в одной нодовой системе, чтобы получить удобный графический инструмент? В программах для создания 3D графики в нодовых редакторах часто используется разделение на контексты, которые обычно плохо совместимы друг с другом. Но в нашем случае структуры данных не настолько сложны и разнородны, чтобы плодить отдельные редакторы. Поэтому я решил создать единое пространство, разделив ноды на категории по технологиям:
- HTML/XML ноды (для вывода HTML/XML тэгов)
- PHP ноды (управление логикой)
- Ноды запросов к базе данных MySQL
Идея состояла в том, чтобы переход между технологиями был незаметен для пользователя. Чтобы соединение нод разных категорий между собой создавало единый поток выполнения программы.
Рассмотрим по порядку все три категории нод.
HTML/XML ноды
Соединяя такие ноды между собой мы получаем простейшую структуру HTML документа. По-умолчанию HTML/XML нода имеет по одному входу и одному выходу. Вход (IN) — то, что помещается внутрь тэга, выход (OUT) — результирующая строка. Таким образом из последнего выхода ноды «HTML» мы получим строку:
<html>
<head>
<meta />
<script />
</head>
<body>
<div>
<div />
<div />
</div>
</body>
</html>
Служебная нода «Merge» используется для возможности повлиять на сортировку входящих соединений. Сортировка доступна в свойствах самой ноды. В последней ноде «HTML» два входящих соединения конкатенируются в том порядке, в котором произошло соединение. Т.е. если сначала подключили «HEAD», а потом «BODY», то в такой последовательности они и появятся в конечной строке.
Атрибуты тэга мы можем прописать в свойствах ноды, но при желании можем выводить их в виде коннекторов для входящих соединений. Например, добавив ноде «my div» коннекторы для атрибутов id и class мы получим такой вид:
<div id="my-id" class="my-class">
<div>my text</div>
</div>
Переменные со строковыми значениями «my-id» и «my-class» попадают в наши атрибуты. Переменная «my text» становится содержимым первого тэга div. Переменные — это уже PHP ноды, о которых речь пойдет чуть ниже.
В качестве коннекторов мы можем выводить и шаблоны для замены текста внутри тэга. Например, если у нас имеется следующий тэг с прописанным содержимым:
<h1>Hello, %user%</h1>
И если шаблон %user% выведен как коннектор, то теперь мы можем заменить этот шаблон переменной следующим образом:
В первом случае текст внутри ноды прописан в ее свойствах и коннектор для данных выключен, а выведен только коннектор для замены шаблона. Во втором случае текст с шаблоном тоже приходит из переменной. И в том и в другом случае на выходе мы получим:
<h1>Hello, Mooha</h1>
Конечно, нет особого смысла заниматься построением HTML кода именно таким образом. В редакторе есть возможность использовать ноды не только с отдельными тэгами, а с целыми кусками кода и даже подключать внешние файлы.
Например, у нас есть файл template.tpl, который содержит два шаблона для замены: %title% и %content%. Содержимое файла следующее:
<html>
<head>
</head>
<body>
<h1>%title%</h1>
<div>%content%</div>
</body>
</html>
Чтобы подключить файл и заменить шаблоны переменными, достаточно построить вот такую схему:
В результате получим:
<html>
<head>
</head>
<body>
<h1>My article title</h1>
<div>My article content</div>
</body>
</html>
Так как мы уже вышли за рамки описания только HTML нод, перейдем к следующему типу — к PHP нодам.
PHP ноды
Это простейший пример арифметических вычислений и вызова встроенной функции abs() с помощью PHP нод. Названия нод произвольные и могут меняться в свойствах каждой ноды. Для удобства я переименовал их так, чтобы были видны значения переменных и названия арифметических операций.
Первые три ноды «1», «2» и «5» — это переменные, значения которых соответствуют названиям.
Т.е. в результате мы получим такой скрипт:
<?php
print abs((1+2)-5);
?>
А на выходе просто число 2.
Я постарался реализовать необходимые логические конструкции, обычно используемые в нодовых системах. Например, вот так выглядит сравнение двух переменных (или результатов двух потоков):
Результат будет true (1!=2).
А вот так можно делать выбор потока, исходя из условий:
Здесь каждому красному коннектору в свойствах ноды присвоены значения, с которыми сравнивается значение, поступившее в коннектор оранжевого цвета. Если значения совпадают, то переключатель Switch пропускает соответствующий поток. В этом примере красные коннекторы имеют значения 1, 2 и 3. То есть результат такой конструкции будет «it was 2».
Во всех цепочках в соединениях передаются конечные значения всех предыдущих вычислений потока. Таким образом в следующем примере у нас результат будет тоже «it was 2», но результирующая строка собирается из нескольких действий:
Генерируется примерно следующий код:
$data = 2;
switch ($data)
{
case "1": // заданное в первом коннекторе значение для сравнения
$result = "it was 1";
break;
case "2":// заданное во втором коннекторе значение для сравнения
$result = str_replace("foo", "2", "in was foo");
break;
case "3": // заданное в третьем коннекторе значение для сравнения
$result = "it was 3";
break;
default:
$result=false;
}
var_dump($result);
Отдельного внимания заслуживает нода Copy, которая по значениям из первого соединения размножает шаблон из второго. Это полезно, например, в том случае, когда нам необходимо размножить HTML код и заменить в нем шаблоны значениями из массива. В самом простом случае с одномерным массивом такая схема выглядит следующим образом:
Для наглядности я назвал ноды значениями их переменных. В первый коннектор (для значений) мы запускаем JSON массив, во второй — HTML код с шаблоном для замены. Все ноды (или цепочки нод), подключенные к коннектору шаблона будут скопированы столько раз, сколько имеется элементов в массиве, подключенному к первому коннектору. При этом у каждой ноды слева от такого соединения появится специальная настройка, какие именно данные в ней заменять элементом из массива. В нашем случае в ноде с параграфом настроена замена шаблона %number%. Т.е. результатом этой схемы будет следующий код:
<p>first paragraph</p>
<p>second paragraph</p>
<p>third paragraph</p>
Вот пример сложнее, с массивом объектов и с несколькими нодами:
В переменной «JSON» следующий массив объектов:
[
{"title":"first title","content":"first content"},
{"title":"second title","content":"second content"}
]
В ноде c тэгом
шаблону %title% в качестве источника для копирования указан объект "title" массива. В ноде с тэгом шаблону %content% в качестве источника для копирования указан объект "content" массива. В результате генерируется следующий код:
<div>
<h1>first title</h1>
<p>first content</p>
</div>
<div>
<h1>second title</h1>
<p>second content</p>
</div>
По умолчанию нода Copy имеет тип foreach и работает так, как я описал в двух примерах выше. Но мы можем выбрать тип for, после чего появится возможность настроить стартовое значение счетчика, конечное значение, шаг и тип инкремента (возрастающий или убывающий).
Ноды запросов к таблицам базы данных MySQL
Базовая нода для запросов — это таблица, а точнее — оператор SELECT, который выполняется по-умолчанию при подключении к любому полю. Присоединившись к последнему выходу, обозначенному символом "*", мы выполним запрос:
SELECT * FROM `authors`
Результат получим в виде массива объектов в формате JSON. Это подключение "поймает" следующий набор:
[
{ "id": "1", "Name": "Iain Menzies Banks", "Birthday": "1954-02-16" },
{ "id": "2", "Name": "Charles Michael Palahniuk", "Birthday": "1962-02-21" }
]
Чтобы получить массив только имен авторов, соединение будет выглядеть следующим образом:
На выходе мы получим обычный одномерный массив:
["Iain Menzies Banks", "Charles Michael Palahniuk"]
На данный момент в редакторе есть несколько служебных SQL нод, с помощью которых можно выполнять несложные выборки. Например:
В первом случае (слева) мы выполним запрос:
SELECT * FROM `authors` WHERE id=1
И получим массив с одним объектом:
[
{ "id": "1", "Name": "Iain Menzies Banks", "Birthday": "1954-02-16" }
]
Во втором случае (справа) запрос будет таким:
SELECT * FROM `authors` WHERE Name LIKE '%Banks%'
А вот запрос с несколькими условиями и сортировкой:
SELECT * FROM `authors` WHERE (`Birthday` < '1990-01-01' AND `Birthday` > '1920-01-01') ORDER BY `Name` ASC
Служебная нода AND выступает здесь не только в качестве логического оператора в условии WHERE, но и в качестве соединения команд в одном запросе (в нашем случае, добавление к запросу команды ORDER BY)
Запросы INSERT, UPDATE и DELETE вызываются подключениями к таким же нодам-таблицам, но только с соответствующими настройками.
Простой INSERT выглядит так:
INSERT INTO `authors` (`id`, `Name`, `Birthday`) VALUES ('1', 'Iain Menzies Banks', '1954-02-16')
UPDATE:
UPDATE `authors` SET `Name`='Iain Banks', `Birthday`='1954-02-17' WHERE `id`='1'
DELETE:
DELETE FROM `authors` WHERE `Birthday`<'1954-02-17'
В процессе построения генерируются запросы с использованием PDO.
С помощью нодовых соединений можно построить почти любые типы запросов, в том числе организовать JOIN объединения. Однако не для всех сложных запросов я нашел идеальные графические решения, поэтому оставил возможность вписать текст SQL инструкции в специальную ноду. Немного забегая вперед, скажу, что работу с базой данных можно построить в системе и другими способами, не только с помощью MySQL нод.
"А теперь вместе!"
И, наконец, пример объединения всех трех технологий (MySQL, PHP и HTML) в одной цепочке, может выглядеть следующим образом:
Здесь у нас имеется заготовленный шаблон library.tpl для вывода книг из домашней библиотеки. Содержимое примерно следующее:
<div>
<select>
<option value=''>all authors</option>
%author-list%
</select>
</div>
<br />
<div>%book-list%</div>
У нас есть два шаблона для замены: %author-list% и %book-list%. В первый нам нужно вывести тэги option, соответствующие всем авторам. Во второй — все книги, а именно — размножить блок "book block", в котором содержится HTML код вывода одной книги. Первый список нам нужен для того, чтобы фильтровать книги по автору (для этого действия мы будем использовать функцию jquery в шаблоне library.tpl).
Для первого шаблона %author-list% мы добавляем ноду Copy ("Author Copy"), которая по данным из таблицы "authors" клонирует тэг "option", при этом внутрь тэга помещает имя автора, а атрибуту "value" присваивает значение id. На выходе ноды "Author Copy" мы получим следующий код:
<option value='1'>Iain Menzies Banks</option>
<option value='2'>Charles Michael Palahniuk</option>
<option value='3'>Mikhail Bulgakov</option>
Нода "book block" содержит следующий код:
<div class='author-%author-id%'>
<img width='100' src='images/%image%' align='left' />
<b>%title%</b> by <b>%author%</b>
<br />
ISBN: %isbn%<br />
<small>%description%</small>
<br clear='all'/>
</div>
Шаблоны в этом блоке заменяются данными книги из объединенной выборки двух таблиц "books" и "authors". Таблица "authors" здесь нужна для вывода в книге имени ее автора. Сам блок клонируется столько раз, сколько у нас записей в таблице "books".
Вот что у нас получится в результате:
Заключение
В этой статье я постарался описать основную идею работы системы. В проекте реализовано много других возможностей. Некоторые из них — типичные для нодовых интерфейсов, некоторые мне пришлось изобретать самому, с учетом специфики редактора. Вот неполный список доступных на данный момент инструментов:
- Создание групп — специальных нод, внутрь которых можно прятать целые схемы и выводить только необходимые входящие и выходящие коннекторы. Глубина вложенности групп неограниченна.
- Возможность выводить переменные из любой вложенной схемы в качестве параметров для группы-родителя.
- Сохранение логических схем или их частей в качестве сниппетов, для повторного использования в других скриптах.
- Возможность написания своих функций, которые можно выводить в редакторе в виде нод.
- Создание своих или подключение готовых классов, возможность вывода методов классов в качестве нод.
- Подключение скриптов (include), как созданных в самой системе, так и скриптов из других файлов.
- Возможность редактирования файлов, используя встроенный редактор.
- Инструменты для создания и редактирования таблиц MySQL.
Результат работы редактора — набор генерированных PHP файлов. Каждая сцена — это отдельный скрипт, сохраненный в специальной папке. Такой скрипт может выполняться без помощи самого редактора. Кроме того, в настройках системы можно выбрать стартовый сценарий, который будет включать в себя главную логику работы всего проекта, исходя из полученных данных (строка запроса или передаваемые параметры).
Цель написания этой статьи — понять, насколько такая система может быть интересна для разработчиков. Я слишком долго и увлеченно занимался этим проектом и у меня изменилось отношение к нему с того момента, как я начал над ним работу. Поэтому я буду крайне признателен за отзывы и комментарии, за вопросы и предложения, а также за участие в тестировании.
На данный момент проект почти закончен и идет процесс отладки. Совсем скоро я буду готов выложить бета-версию. Для того, чтобы быть в курсе процесса разработки и узнать о выходе проекта — нужно просто подписаться на получение новостей любым удобным способом на сайте Mooha.net.
Техническая информация
В проекте использованы следующие инструменты:
- HTML5 Canvas и Javascript (нодовый редактор)
- JQuery 2.0 (панели свойств и настроек)
- Codemirror (встроенный редактор файлов)
- CKEditor (расширенный редактор полей таблиц базы данных)
- Spectrum Colorpicker (изменение цвета элементов нод)
- jQuery File Tree Plugin (браузер файловой системы)
Серверная часть тестировалась в таких условиях:
- Apache 2.2.22
- PHP 5.4.3
- MySQL 5.5.24