Базы данных

Подключение к базе данных

F3 разработан с уётом того, чтобы упростить работу с базами данных SQL. Если вы не из тех, кто погружается в подробности SQL, но больше склоняетесь к объектно-ориентированной обработке данных, вы можете сразу перейти к следующему разделу этого руководства. Однако, если вам нужно выполнить некоторые сложные задачи по обработке данных и оптимизации производительности базы данных, SQL - это то, что вам нужно.

Подключение к таким БД как MySQL, SQLite, PostgreSQL, SQL Server, Sybase и Oracle, осуществляется с помощью знакомой $f3→set()команды.

Подключение к базе данных SQLite будет выглядеть так:

$f3->set('DB', new DB\SQL('sqlite:/absolute/path/to/your/database.sqlite'));

Теперь вы можете работать с объектом БД из любого места вашего приложения с помощью $f3→get('DB')→exec('…');.

Другой пример, на этот раз с MySQL:

$db=new DB\SQL(
    'mysql:host=localhost;port=3306;dbname=mysqldb',
    'admin',
    'p455w0rD'
);

Запросы к БД

Подключение к БД прошло легко, так же как это можно было сделать и в обычном PHP. Вам просто нужно знать формат DSN базы данных, к которой вы подключаетесь. См. Раздел PDO в руководстве по PHP на php.net.

Продолжим на примере:

$f3->set('result',$db->exec('SELECT brandName FROM wherever'));
echo Template::instance()->render('abc.htm');

В этом примере мы не должны настраивать такие вещи, как PDO, операторы, курсоры и т.д.? Ответ: нет. F3 все упрощает, беря на себя всю тяжелую работу.

Для примера выше создаем такой HTML-шаблон, abc.htm который содержит как минимум следующее:

<repeat group="{{ @result }}" value="{{ @item }}">
    <span>{{ @item.brandName  }}</span>
</repeat>

В большинстве случаев набора команд SQL должно быть достаточно для создания готового к работе сайта, поэтому вы можете использовать result напрямую в своем шаблоне. Fat-Free позволяет параллельно использовать любые команды SQL. Фактически, DB\SQL класс в F3 является производным непосредственно от PDO класса PHP (обёртка), поэтому у вас все еще есть доступ к базовым компонентам PDO, если вам нужен более продвинутый функционал.

Трансакции

Вместо одного оператора, предоставленного в качестве аргумента $db→exec()команды, вы также можете передать массив операторов SQL, например:

$db->exec(
    array(
        'DELETE FROM diet WHERE food="cola"',
        'INSERT INTO diet (food) VALUES ("carrot")',
        'SELECT * FROM diet'
    )
);

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

Вы также можете запускать и завершать транзакцию вручную:

$db->begin();
$db->exec('DELETE FROM diet WHERE food="cola"');
$db->exec('INSERT INTO diet (food) VALUES ("carrot")');
$db->exec('SELECT * FROM diet');
$db->commit();

Откат (роллбек) произойдет, если какой-либо из операторов обнаружит ошибку.

Чтобы получить список всех инструкций базы данных, выполните:

echo $db->log();

Параметризованные запросы

Передача строковых аргументов в операторы SQL чревата инъекциями, иными словами взломом и повреждением данных. Например:

$db->exec(
    'SELECT * FROM users '.
    'WHERE username="'.$f3->get('POST.userID').'"'
);

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

admin"; DELETE FROM users; SELECT "1

К счастью, параметризованные запросы помогают снизить эти риски:

$db->exec(
    'SELECT * FROM users WHERE userID=?',
    $f3->get('POST.userID')
);

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

Пример из предыдущего раздела будет намного безопаснее, если будет написан таким образом:

$db->exec(
    array(
        'DELETE FROM diet WHERE food=:name',
        'INSERT INTO diet (food) VALUES (?)',
        'SELECT * FROM diet'
    ),
    array(
        array(':name'=>'cola'),
        array(1=>'carrot'),
        NULL
    )
);

CRUD

F3 имеет в своём составе технологию программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных» (ORM), иными словами, компоненты, которые находятся между вашим приложением и вашими данными, что значительно упрощает и ускоряет написание программ, которые обрабатывают общие операции с данными, такие как создание, извлечение, обновление и удаление (CRUD) информации из баз данных SQL и NoSQL. Преобразователи данных(Mapper-ы) выполняют большую часть работы, сопоставляя взаимодействия объектов PHP с соответствующими внутренними запросами.

Предположим, у вас есть существующая база данных MySQL, содержащая таблицу пользователей вашего приложения. (SQLite, PostgreSQL, SQL Server, Sybase также подойдут.) Он был бы создан с помощью следующей команды SQL:

CREATE TABLE users (
    userID VARCHAR(30),
    password VARCHAR(30),
    visits INT,
    PRIMARY KEY(userID)
);

Примечание. MongoDB - имеет механизм NoSQL, который по своей сути не имеет схемы. F3 имеет собственную быструю и легкую реализацию NoSQL под названием Jig, которая использует сериализованные PHP или JSON-кодированные flat файлы. Эти уровни абстракции не требуют жестких структур данных. Поля могут отличаться от одной записи к другой. Их также можно определить или удалить на лету.

А теперь вернемся к SQL. Сначала мы устанавливаем связь с нашей базой данных.

$db=new DB\SQL(
    'mysql:host=localhost;port=3306;dbname=mysqldb',
    'admin',
    'wh4t3v3r'
);

Чтобы получить запись из нашей таблицы пишем:

$user=new DB\SQL\Mapper($db,'users');
$user->load(array('userID=?','tarzan'));

Первая строка создает экземпляр объекта Mapper, который взаимодействует с таблицей users в нашей базе данных. F3 извлекает структуру таблицы users и определяет, какие поля определены как первичный ключ (а). На данный момент объект Mapper еще не содержит никаких данных («сухое состояние»), а переменная $user, по сути, не более чем структурированный объект, но она содержит методы, необходимые для выполнения основных операций CRUD, а также некоторые дополнительные функции. как вы увидите позже. Теперь, чтобы получить запись из нашей таблицы users, например, с полем, userID содержащим строковое значение tarzan, мы используем метод load(). Этот процесс называется «автоматическим увлажнением» (заполнением пустого объекта) объекта отображения данных.

F3 понимает, что таблица SQL уже имеет структурное определение, существующее в самом ядре базы данных. В отличие от других фреймворков, F3 не требует дополнительных объявлений классов (если вы не хотите расширять Mapper для соответствия более сложным объектам), никаких избыточных свойств массива/объекта PHP с полями, никаких генераторов кода, никаких файлов XML / YAML для настройки ваших моделей, никаких лишних команд для получения только одной записи. С помощью F3 простое изменение размера поля varchar в вашей таблице MySQL не требует никаких изменений кода вышего приложения. В соответствии с MVC и «разделением задач», администратор базы данных имеет такой же контроль над данными и структурами, как дизайнер шаблонов над шаблонами HTML/XML.

Если вы предпочитаете работать с базами данных NoSQL, сходство в синтаксисе запросов будет поверхностным. Работа с Mapper-ом MongoDB будет выглядеть так:

$db=new DB\Mongo('mongodb://localhost:27017','testdb');
$user=new DB\Mongo\Mapper($db,'users');
$user->load(array('userID'=>'tarzan'));

С Jig синтаксис похож на синтаксис шаблонизатора F3:

$db=new DB\Jig('db/data/',DB\Jig::FORMAT_JSON);
$user=new DB\Jig\Mapper($db,'users');
$user->load(array('@userID=?','tarzan'));

Smart SQL ORM

F3 автоматически представляет поле visits из вашей таблицы как свойство Mapper в ходе его инициализации $user=new DB\SQL\Mapper($db,'users');

После того, как объект создан, $user→password и $user→userID определены как password и userID поля в нашей таблице, соответственно. Иными словами, получая свойство password вы автоматически получаете значение поля password из БД.

Вы не можете добавить или удалить сопоставленное поле или изменить структуру таблицы с помощью ORM. Вы должны сделать это в MySQL или другом движке базы данных, который вы используете. После того, как вы внесли изменения в ядро ​​базы данных, F3 автоматически синхронизирует новую структуру таблицы с вашим объектом отображения данных (Mapper) при запуске приложения.

F3 извлекает структуру преобразователя данных (Mapper-а) непосредственно из схемы БД. Он понимает различия между механизмами баз данных MySQL, SQLite, MSSQL, Sybase и PostgreSQL.

Примечание: идентификаторы SQL не должны использовать зарезервированные слова, и должен быть ограничен символами A-Z, 0-9 и символом подчеркивания (_). Имена столбцов, содержащие пробелы (или специальные символы) и заключенные в кавычки несовместимы с ORM. Они не могут быть правильно представлены как свойства объекта PHP.

Допустим, мы хотим увеличить количество посещений страницы пользователем в users и обновить соответствующую запись в нашей таблице:

$user->visits++;
$user->save();

Если мы хотим вставить запись:

$user=new DB\SQL\Mapper($db,'users');
// or $user=new DB\Mongo\Mapper($db,'users');
// or $user=new DB\Jig\Mapper($db,'users');
$user->userID='jane';
$user->password=md5('secret');
$user->visits=0;
$user->save();

Мы используем тот же метод save(). Но как F3 узнает, когда запись должна быть вставлена ​​или обновлена? В то время как объект сопоставления данных (Mapper) автоматически наполняется при извлечении записи, фреймворк отслеживает первичные ключи записи (или _id, в случае MongoDB и Jig), поэтому знает, какую запись следует обновить или удалить, даже если значения первичных ключей изменяются. Mapper, значения которого не были извлечены из базы данных, но были заполнены приложением - не будет иметь памяти предыдущих значений в своих первичных ключах. То же самое относится к MongoDB и Jig, но с использованием объекта _id в качестве ссылки. Итак, когда мы создали экземпляр $user объект выше и заполнил его свойства значениями из нашей программы - вообще не извлекая запись из пользовательской таблицы, F3 знает, что она должна вставить эту запись.

Объект сопоставления (Mapper) не будет пустым после выполнения save(). Если вы хотите добавить новую запись в свою базу данных, вы должны сначала очистить Mapper, используя метод reset():

$user->reset();
$user->userID='cheetah';
$user->password=md5('unknown');
$user->save();

Повторный вызов save() без вызова reset() просто обновит запись, на которую в данный момент указывает Mapper.

Особенности SQL

Хотя проблема наличия первичных ключей во всех таблицах вашей базы данных носит спорный характер, F3 не мешает вам создать объект сопоставления данных, который взаимодействует с таблицей, не содержащей первичных ключей. Единственный недостаток: вы не можете удалить или обновить сопоставленную запись, потому что F3 абсолютно не может определить, на какую запись вы ссылаетесь. Идентификаторы строк не переносятся между различными механизмами SQL и могут не возвращаться драйвером базы данных PHP.

Чтобы удалить сопоставленную запись из нашей таблицы, вызовите метод erase() в Mapper. Например:

$user=new DB\SQL\Mapper($db,'users');
$user->load(array('userID=? AND password=?','cheetah','ch1mp'));
$user->erase();

Синтаксис запроса Jig будет таким:

$user=new DB\Jig\Mapper($db,'users');
$user->load(array('@userID=? AND @password=?','cheetah','chimp'));
$user->erase();

И эквивалент MongoDB:

$user=new DB\Mongo\Mapper($db,'users');
$user->load(array('userID'=>'cheetah','password'=>'chimp'));
$user->erase();

Состояние данных Mapper-а

Чтобы узнать, загружена-ли в наш сопоставитель данных (Mapper) действительная запись данных, используйте метод dry():

if ($user->dry())
    echo 'No record matching criteria';

Не CRUD методы

Мы рассмотрели обработчики CRUD. Есть несколько дополнительных методов, которые могут вам пригодиться:

$f3->set('user',new DB\SQL\Mapper($db,'users'));
$f3->get('user')->copyFrom('POST');
$f3->get('user')->save();

Обратите внимание, что мы также можем использовать переменные F3 в качестве контейнеров для объектов сопоставления Mapper. Метод copyFrom() позволяет сопоставить элементы массива из F3 с БД в таблице которой имена полей соответсвуют именам ключей этого массива. В примере выше, например веб-форма отправляется (при условии, что атрибут имени HTML установлен в userID), содержимое этого поля ввода передается $_POST['userID'], дублируется F3 в переменной POST.userID и сохраняется в сопоставленном поле $user→userID в базе данных. Процесс становится очень простым, если все они имеют идентично названные элементы. Согласованность в ключах массива, т.е. именах токенов шаблона, именах переменных фреймворка и именах полей является ключевым моментом, на котором основывается подобный функционал.

Внимание! По-умолчанию метод copyfrom() принимает весь предоставленный массив. Это может вызвать проблему безопасности, если пользователь передаст больше полей, чем вы ожидаете. Используйте 2-й параметр, чтобы настроить функцию callback, чтобы избавиться от нежелательных полей.

С другой стороны, если мы хотим получить запись и скопировать значения полей в переменную F3 для дальнейшего использования, например отрисовки шаблона:

$f3->set('user',new DB\SQL\Mapper($db,'users'));
$f3->get('user')->load(array('userID=?','jane'));
$f3->get('user')->copyTo('POST');

Затем мы можем назначить

{{@ POST.userID}}

атрибуту того поля ввода. HTML будет выглядеть так:

<input type="text" name="userID" value="{{ @POST.userID }}">

Методы Mapper-a save(), update(), copyFrom() и параметризованные варианты load() и erase() защищены от SQL инъекций.

Навигация и разбивка на страницы

По умолчанию метод load() извлекает только первую запись, которая соответствует указанным критериям. Если у вас более одной записи, удовлетворяющей тому же условию, что и загруженная первая запись, вы можете использовать метод skip() для навигации:

$user=new DB\SQL\Mapper($db,'users');
$user->load('visits>3');
// Rewritten as a parameterized query
$user->load(array('visits>?',3));
 
// For MongoDB users:
// $user=new DB\Mongo\Mapper($db,'users');
// $user->load(array('visits'=>array('$gt'=>3)));
 
// If you prefer Jig:
// $user=new DB\Jig\Mapper($db,'users');
// $user->load('@visits>?',3);
 
// Display the userID of the first record that matches the criteria
echo $user->userID;
// Go to the next record that matches the same criteria
$user->skip(); // Same as $user->skip(1);
// Back to the first record
$user->skip(-1);
// Move three records forward
$user->skip(3);

Вы, так же, можете использовать $user→next() и $user→prev() в качестве замены $user→skip().

Используйте dry(), чтобы проверить, не вышли ли вы за пределы набора результатов. Метод dry() вернет TRUE, если вы выберите с помощью skip(-1) первую запись. Он также вернет TRUE, если вы выбрали c помощью skip(1) последнюю запись, отвечающую критериям поиска.

Метод load() принимает второй аргумент: массив содержащий пары key⇒value:

$user->load(
    array('visits>?',3),
    array(
        'order'=>'userID DESC',
        'offset'=>5,
        'limit'=>3
    )
);

Если вы используете MySQL, запрос преобразуется в:

SELECT * FROM users
WHERE visits>3
ORDER BY userID DESC
LIMIT 3 OFFSET 5;

Это один из способов представления данных небольшими порциями. Вот еще один способ разбивки результатов на страницы:

$page=$user->paginate(2,5,array('visits>?',3));

В приведенном выше коде F3 будет извлекать записи, соответствующие критериям 'visits>3'. Затем он ограничит результаты до 5 записей (на страницу), начиная со смещения страницы 2 (отсчитывается от 0). Фреймворк вернет массив, состоящий из следующих элементов:

[subset] array of mapper objects that match the criteria
[total] sum of all records for all pages
[limit] same value as the size parameter (here 5)
[count] number of of subsets/pages available
[pos] actual subset position

Ответ может быть NULL, если первый аргумент paginate() является отрицательным числом или превышает количество найденных подмножеств.

Виртуальные поля

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

Предположим, у нас есть следующая таблица:

CREATE TABLE products (
    productID VARCHAR(30),
    description VARCHAR(255),
    supplierID VARCHAR(30),
    unitprice DECIMAL(10,2),
    quantity INT,
    PRIMARY KEY(productID)
);

Поля totalprice не существует, поэтому мы можем указать F3 запросить у БД произведение двух полей:

$item=new DB\SQL\Mapper($db,'products');
$item->totalprice='unitprice*quantity';
$item->load(array('productID=:pid',':pid'=>'apple'));
echo $item->totalprice;

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

У вас могут быть более сложные виртуальные поля:

$item->mostNumber='MAX(quantity)';
$item->load();
echo $item->mostNumber;

На этот раз F3 получает имя товара с наибольшим количеством на складе (обратите внимание, что метод load() не определяет никаких критериев, поэтому все записи в таблице будут обработаны).

Вы также можете получить значение из другой таблицы:

$item->supplierName=
    'SELECT name FROM suppliers '.
    'WHERE products.supplierID=suppliers.supplierID';
$item->load();
echo $item->supplierName;

Каждый раз, когда вы загружаете запись из таблицы товаров, работают перекрестные ссылки ORM на supplerID в таблице products с supplierID в таблице suppliers.

Чтобы уничтожить виртуальное поле, используйте unset($item→totalPrice); Выражение isset($item→totalPrice); возвращает значение TRUE , если totalPrice виртуальное поле было определено, или FALSE, если его не существует.

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

Поиск

Если вам не нужна пошаговая навигация, вы можете получить весь пакет записей за один раз:

$frequentUsers=$user->find(array('visits>?',3),array('order'=>'userID'));

Синтаксис запроса Jig mapper:

$frequentUsers=$user->find(array('@visits>?',3),array('order'=>'userID'));

Эквивалентный код для MongoDB:

$frequentUsers=$user->find(array('visits'=>array('$gt'=>3)), array('order'=>array('userID'=>1)));

Метод find() ищет в таблице users записи, которые соответствуют критериям, сортирует результат по userID и возвращает результат в виде массива объектов Mapper. find('visits>3')отличается от load('visits>3'). Последний относится к текущему объекту $user. find() не влияет на skip().

Важно!: объявление пустого условия, NULL или строки нулевой длины в качестве первого аргумента методов find() или load() приведет к извлечению всех записей. Убедитесь, что вы знаете, что делаете - вы можете превысить значение досутпной памяти PHP(memory_limit).

Метод find() имеет следующий синтаксис:

find(
    $criteria,
    array(
        'group'=>'foo',
        'order'=>'foo,bar',
        'limit'=>5,
        'offset'=>0
    )
);

find () возвращает массив объектов. Каждый объект является Mapper-ом, соответствующим указанным критериям:

$place=new DB\SQL\Mapper($db,'places');
$list=$place->find('state="New York"');
foreach ($list as $obj)
    echo $obj->city.', '.$obj->country;

Если вам нужно преобразовать объект Mapper-a в ассоциативный массив, используйте метод cast():

$array=$place->cast();
echo $array['city'].', '.$array['country'];

Чтобы получить количество записей в таблице, соответствующих определенному условию, используйте метод count().

if (!$user->count(array('visits>?',10)))
    echo 'We need a better ad campaign!';

Также существует метод select(), похожий на, find(), но обеспечивающий более точный контроль над возвращаемыми полями. Его синтаксис похож на SQL:

SELECT(
    'foo, bar, baz',
    'foo > ?',
    array(
        'group'=>'foo, bar',
        'order'=>'baz ASC',
        'limit'=>5,
        'offset'=>3
    )
);

Как и метод find(), select() не изменяет содержимое объекта Mapper-a. Он служит только удобным методом для запроса сопоставленной таблицы. Возвращаемое значение обоих методов - это массив объектов Mapper. Использование dry() для определения того, была ли найдена запись одним из этих методов, неуместно. Если ни одна запись не соответствует критериям find() или select(), возвращаемое значение - пустой массив, что легко проверить любым удобным способом на php.

Внимание: массив $options не использует параметризованные поля и не предусматривает фильтрацию входных данных. Не забудьте обеспечить фильтрацию во избежание проблес с уязвимостями.

Профилирование

Если вы хотите узнать, какие операторы SQL использовались в ходе выполнения Вашего приложения(прямо или косвенно через объекты Mapper), с целью оценки производительсности, примените такой метод:

echo $db->log();

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

Если Mapper-a мало

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

class Vendor extends DB\SQL\Mapper {
	// Instantiate mapper
	function __construct(DB\SQL $db) {
		// This is where the mapper and DB structure synchronization occurs
		parent::__construct($db,'vendors');
	}
 
	// Specialized query
	function listByCity() {
		return $this->select('vendorID,name,city',null,array('order'=>'city DESC'));
		/*
		We could have done the same thing with plain vanilla SQL:
		return $this->db->exec(
			'SELECT vendorID,name,city FROM vendors '.
			'ORDER BY city DESC;'
		);
		*/
	}
}
 
$vendor=new Vendor;
$vendor->listByCity();

За и против

Если вы хорошо разбираетесь в SQL, вы, вероятно, скажете: все в ORM можно обрабатывать с помощью SQL-запросов. Действительно, мы можем обойтись без дополнительных служб которые слушают события, используя триггеры базы данных и хранимые процедуры. Мы можем выполнять реляционные запросы с объединенными таблицами. ORM - это просто ненужные накладные расходы. Но дело в том, что Mapper-ы предоставляют вам дополнительную функциональность по использованию объектов для представления сущностей базы данных. Как разработчик, вы можете писать код быстрее и работать более продуктивно. Полученная программа будет чище, и иногда даже короче. Но вам придется взвесить преимущества и недостатки, компромисс в скорости - особенно при работе с большими и сложными хранилищами данных. Помните, что все ORM - какими бы лёгкими и быстрыми они ни были - всегда будут просто еще одним уровнем абстракции. Им все еще нужно передать работу базовым механизмам SQL, а это значит что они будут медленнее классических SQL запросов.

Прежде чем объединять несколько объектов в своем приложении для управления базовыми таблицами в базе данных, подумайте об этом: создание Mapper для представления отношений и триггеров для определения поведения объектов в БД более эффективно. Механизмы реляционных баз данных предназначены для обработки представлений, объединенных таблиц и триггеров. Это не тупые хранилища данных. Таблицы, объединенные в представление, будут отображаться как одна таблица, а Fat-Free может автоматически отображать Mapper так же, как и обычную таблицу. Репликация JOIN в качестве реляционных объектов в PHP происходит медленнее по сравнению с машинным кодом ядра СУБД, реляционной алгеброй и логикой оптимизации. Кроме того, многократное объединение таблиц в нашем приложении является верным признаком того, что проект базы данных требует аудита, а представления считаются неотъемлемой частью поиска данных. Если таблица часто ссылается на данные из другой таблицы, рассмотрите возможность нормализации структур или создания представления. Затем создайте объект сопоставления для автоматического сопоставления этого вида. Это быстрее и требует меньше усилий.

Рассмотрим это представление SQL, созданное внутри вашего механизма базы данных:

CREATE VIEW combined AS
    SELECT
        projects.project_id AS project,
        users.name AS name
    FROM projects
    LEFT OUTER JOIN users ON
        projects.project_id=users.project_id AND
        projects.user_id=users.user_id;

Код вашего приложения становится простым, потому что ему не нужно поддерживать два объекта Mapper (один для таблицы проектов, а другой для пользователей) только для получения данных из двух объединенных таблиц:

$combined=new DB\SQL\Mapper($db,'combined');
$combined->load(array('project=?',123));
echo $combined->name;

Совет: используйте инструменты, для которых они предназначены. У Fat-Free уже есть простой в использовании помощник SQL. Постарайтесь найти баланс между удобством и производительностью. SQL всегда будет вашим запасным вариантом, если вы работаете со сложными и устаревшими структурами данных.

Печать/экспорт