Repository navigation
Releases: visavi/motor-orm
Release list
Version 6.0.1
6.0.1
Патч к 6.0. Обновление ничего не ломает, менять код не нужно.
Пустой ключ — это null, а null не индекс массива
Пустая колонка читается как null, а PHP 8.5 объявил использование null в качестве индекса массива устаревшим. Загрузчик связей брал $byKey[$row->attr[$localKey] ?? null], поэтому строка с
незаполненным ключом — сообщение от гостя, запись без владельца, что угодно с пустой колонкой — давала предупреждение на каждое чтение связи:
Deprecated: Using null as an array offset is deprecated, use an empty string instead
in .../src/RelationLoader.php on line 76
null не совпадает ни с чем, и ровно это такая строка и получала раньше: пустой hasOne, пустую коллекцию в остальных случаях. Теперь она не участвует в поиске, вместо того чтобы искаться по
null:
$localId = $row->attr[$relation->localKey] ?? null;
$found = $localId === null ? null : ($byKey[$localId] ?? null);Закрыты все четыре места, а не только то, на которое указывало предупреждение: та же ошибка была на стороне связанных записей и дважды в hasManyThrough — при сборке промежуточной таблицы и при
выборке по ней.
Поведение связей не изменилось, изменилось только отсутствие предупреждений.
Version 6.0.0
Ломающие изменения
Модель снова строка таблицы, Record убран. Пятая версия разделила модель
на три класса и оставила поведение строки без места: колонка принадлежала
Record, поэтому всё, что строка умеет сказать о себе, приходилось держать
классом рядом с моделью. Одна таблица — два файла, и чем меньше модель, тем
дороже обходилось разделение.
Модель объявляет таблицу и, прочитанная, держит значения одной строки. Что
строка умеет отвечать — метод модели, рядом с колонками.
class Article extends Model
{
public string $table = 'articles.csv';
public function excerpt(int $words = 30): string
{
return implode(' ', array_slice(explode(' ', $this->text), 0, $words));
}
}
$article = Article::query()->find(1); // Article
$article->excerpt();Найденное пятой версией осталось: строка не несёт условий. where(),
orderBy(), limit() принадлежат Query, поэтому строка не читает таблицу
сама по себе, а связь по-прежнему только метод, объявивший возврат Relation.
find(), first(), get(), cursor(), create() и связи отдают модель.
fresh(), save(), delete(), update(), key(), toArray(),
relationLoaded() и newQuery() — те же методы на модели. RecordMapper стал
RowMapper.
Строка несёт декларацию вместе со значениями, поэтому стоит немного дороже
записи: около 790 байт против 690 на пяти колонках, 377 МБ против 347 на
500 000 строк, прочитанных целиком. Чтение страницы, обход cursor() и подсчёт
не изменились.
Ничего не читается из окружения. resolveCurrentPage() лез в $_GET,
поэтому orm, читающая csv, имела мнение о http, а ответ зависел от того, где
выполняется код: консольная команда, воркер очереди и тест получали то, что
оставил после себя последний запрос. Страницу называет page() или резолвер
приложения, без них имеется в виду первая.
// один раз, в бутстрапе приложения
Pagination::resolvePageUsing(static fn (string $name) => $_GET[$name] ?? 1);links() и встроенный шаблон Bootstrap 5 убраны. Orm везла в комплекте
разметку под css-фреймворк, а рендер вместе с буферизацией вывода жил в
библиотеке данных. Навигацию отдаёт pages() объектами Page, печатает её
приложение:
echo $view->render('pagination', ['pages' => $articles->pages()]);Экранирование ушло туда же, где печать: Page::$url — обычная строка,
экранирует её тот шаблон, который её выводит.
Collection::__toString() убран. Печать коллекции возвращала
Collection@0000..., так что <?= $stories ?> в шаблоне тихо давал хеш вместо
ошибки.
Добавлено
save()вставляет строку, у которой ещё нет ключа, поэтому собранная
руками модель пишется так же, как прочитанная.
$article = new Article();
$article->title = 'Новый заголовок';
$article->save(); // вставлена, ключ выдала таблицаModel::newRow()— строка таблицы с заданными значениями, и
Model::primaryKey()— имя колонки, в которой лежат ключи.Table::primaryKey()— та же колонка, в единственном классе, который
владеет колонками. Раньше правило «ключ — первая колонка» зналиQuery,
TableWriter,RowMapperиModel, каждый сам по себе.
Исправлено
pluck() больше не теряет строки с пустой колонкой. array_column()
спрашивает у объекта __isset(), а он для null отвечает false, поэтому строка
с пустым значением выпадала из результата вместе со своим ключом: таблица
настроек из 43 строк, одна из которых пустая, отдавала 42. Значения читаются
напрямую, пустая колонка остаётся на своём месте со значением null.
Обновление
composer require visavi/motor-orm:^6.0
Требуется PHP 8.5. Формат файлов таблиц не менялся — миграция данных не нужна.
Version 5.4.0
Модель называет класс, в который читаются её строки
Запись держит значения строки, поэтому всё, что строка таблицы умеет
отвечать, принадлежит записи. Класс написать было можно, а вернуть из
запроса нельзя: Query создавал Record по имени класса, записанному руками
в пяти местах, и наследник нигде не появлялся.
Model::$record называет класс, newRecord() его создаёт, recordClass()
отдаёт имя. Запрос спрашивает модель везде, где делает запись: find(),
first(), get(), cursor(), create() и связь, никого не нашедшая. Имя, не
являющееся Record, выбрасывает UnexpectedValueException.
Класс проверяется один раз на модель: результат читается одним и тем же
классом, а проверка на строку стоила бы поиска на строку. Модель, ничего
не назвавшая, читается в Record, как раньше.
Багфикс (src/Collection.php, tests/CollectionTest.php):
pluck() больше не теряет записи с пустой колонкой
array_column() над объектами спрашивает у них __isset(), а он для null
отвечает false, поэтому строка с пустым значением выпадала из результата
целиком — вместе со своим ключом. Таблица настроек из 43 строк, где одно
значение пустое, давала 42 ключа, и чтение недостающего роняло вызвавший
код.
Значения читаются обходом: пустая колонка — колонка тоже, и запись
остаётся на своём месте со значением null. Массивы, которые тоже кладут
в коллекцию, читаются по-прежнему.
Version 5.3.0
Производительность
Поиск по ключу. find() больше не читает всю таблицу. Ключи в файле
идут по возрастанию, поэтому нужную строку находит двоичный поиск. На
50 000 строк запись из конца файла находится за 2.1 мс вместо 73.6.
Отсортированности файла никто не требует: если он отсортирован иначе или
повреждён, find() читает таблицу построчно, как раньше, и отдаёт тот же
ответ. Условия, сортировка или смещение перед find() тоже возвращают
обычное чтение.
Article::query()->find(1); // двоичный поиск
Article::query()->where('name', 'Миша')->find(1); // обычное чтение
Подсчёт строк. count() без условий больше не разбирает строки, а
только считает их. На 50 000 строк 54.5 → 7.3 мс, paginate(10)
55.1 → 7.8 мс; на 500 000 строк 551 → 64 мс. Счёт с условиями не
изменился — ему нужны значения.
Открытие файла. Запрос открывает таблицу один раз вместо двух: имена
колонок берутся из уже открытого файла. Чтение десяти строк 0.083 → 0.061 мс.
Добавлено
- CsvFile::countRows() — сколько строк в файле, без их разбора
- CsvFile::rowFrom() — первая целая строка, начинающаяся с байта или после
- CsvFile::startsRecord() — начинается ли запись с байта
- CsvFile::tell(), CsvFile::size() — позиция и размер файла
- Table::countRecords() — сколько строк в таблице, без заголовка
- Table::headersFrom() — имена колонок из уже открытого файла
- KeySearch — поиск строки по первичному ключуVersion 5.2.0
Готовность к PHP 8.6
PHP 8.6 объявляет устаревшими SplFileObject::fgetcsv(), fputcsv(),
setCsvControl() и getCsvControl(). Csv — единственное, ради чего orm вообще
открывал файл, поэтому SplFileObject из пакета убран целиком.
На его месте MotorORM\CsvFile — SeekableIterator поверх обычного
дескриптора. Разбор csv остался за движком: процедурные fgetcsv() и
fputcsv() депрекейт не затрагивает.
Читает быстрее
Строки таблицы отдаёт генератор поверх fgetcsv, а не SplFileObject,
завёрнутый в LimitIterator и CallbackFilterIterator. Строка стоит одно
возобновление генератора вместо вызова каждого метода трёх итераторов.
На 50 000 строк, против того же кода на голом php:
| операция | было | стало |
|---|---|---|
| выбрать строки по условию | ×1.24 | ×1.07 |
| обойти всю таблицу | ×1.38 | ×1.23 |
| десять последних с сортировкой | ×0.80 | ×0.73 |
На 500 000 строк: cursor() 890 → 785 мс, count() 633 → 551 мс,
paginate(10) 638 → 556 мс.
Ломающие изменения
Model::file(), Model::createFile() и Table::file() возвращают CsvFile.
Csv-контроль теперь в конструкторе, а не в сеттере.
// 5.1
$file = $model->file(); // SplFileObject
// 5.2
$file = $model->file(); // MotorORM\CsvFile
$file->fputcsv(['id', 'title']); // вызов тот же
foreach ($file->rows(1) as $line => $row) {} // без заголовка, номер строки => строка
Table::rewrite() отдаёт замыканию номер строки, а не читаемый файл.
// 5.1
$table->rewrite(function (array &$current, SplFileObject $target, SplFileObject $source) {
if ($source->key() === 0) { /* заголовок */ }
});
// 5.2
$table->rewrite(function (array &$current, CsvFile $target, int $line) {
if ($line === 0) { /* заголовок */ }
});
paginate() и simplePaginate() больше не принимают страницу. Страницу
называет page() или берёт из ?page= запроса.
// 5.1
Article::query()->paginate(10, (int) ($_GET['page'] ?? 1));
// 5.2
Article::query()->paginate(10); // страница из запроса
Article::query()->page(3)->paginate(10); // страница названа явно
setPageName() стал статическим и ничего не возвращает. Имя параметра
нужно знать до того, как появится страница строк, поэтому оно не может
принадлежать этой странице.
// 5.1
$articles->setPageName('p')->links();
// 5.2
Pagination::setPageName('p');
Добавлено
- Query::page() — страница пагинации, названная явно.
- PagedCollection::resolvePageUsing() — откуда берётся текущая страница.
Замыканию передаётся имя параметра, null возвращает чтение из query string.
- PagedCollection::resolveCurrentPage() — запрошенная страница, никогда
ниже первой.
- PagedCollection::pageName() — имя параметра страницы.
- with() принимает замыкание на связь. Связь, названная ключом замыкания,
сужается для одного этого чтения — как constrain() сужает каждое. Если есть
оба, условия складываются, объявленное первым.
Story::query()
->with([
'user',
'comments' => static fn (Query $query) => $query->where('approved', 1),
])
->get();
Обновление
composer require visavi/motor-orm:^5.2
Требуется PHP 8.5+. Формат файлов таблиц не менялся — миграция данных не нужна.Version 5.1.0
Добавлено замыкание в with() и чтение страницы пагинации из запроса
В with() можно передать замыкание, сужающее связь на одну загрузку: имя
ключом, замыкание значением, обычные имена лежат в том же списке.
Story::query()->with([
'user',
'comments' => static fn (Query $query) => $query->where('approved', 1),
])->get();
constrain() остаётся: он описывает связь, которая всегда такая, и работает
при ленивом чтении. Когда есть оба, условия складываются.
Из paginate() и simplePaginate() убран номер страницы, вместо него добавлен
Query::page().
Article::query()->paginate(10); // ?page= из запроса
Article::query()->page(3)->paginate(10); // сказано прямо
В PagedCollection добавлены статические resolvePageUsing(),
resolveCurrentPage() и pageName(), а setPageName() стал статическим:
страницу надо знать до того, как появится сама страница строк. По
умолчанию страница читается из $_GET, resolvePageUsing() подменяет
источник для сред, где $_GET не заполняется.
Ломающее: второй аргумент paginate() и simplePaginate() убран совсем,
setPageName() больше не вызывается на объекте.
Version 5.0.0
A model is no longer the record it reads. The library is now three things — a
model that declares the table, a query that reads it and a record that holds the
values. Requires PHP 8.5.
Breaking
Builderis gone, replaced byModel,QueryandRecord. A model no
longer doubles as the row it returns,refresh()on a record is nowfresh().- Relations declare their return type. Only a method returning
Relation
counts as one, so reading a property never runs something else. - Patterns are methods:
whereLike(),whereNotLike(),orWhereLike(),
orWhereNotLike(). Thelikeoperator is no longer accepted bywhere(), and
an unknown operator throws instead of quietly matching nothing. - Sorting takes
SortOrder,SORT_ASCandSORT_DESCare gone. - Pagination is rebuilt and no longer reads
$_GET.CollectionPaginateis
replaced byPaginationandSimplePagination; the page number is passed in:
paginate(10, $page). - Migrations are written as a chain,
createTable()andchangeTable()no
longer take a closure. - A broken cast is an error: a column cast to
arrayorobjectthat does
not hold json throws instead of reading back asnull. Collectionlostkey(),next()andcurrent().- The csv escape character is gone. Files follow RFC 4180: a quote inside a
value is doubled, nothing is escaped with a backslash. A table written by 4.x
whose values end in a backslash may read differently — check such tables
before upgrading.
Added
cursor(), simplePaginate(), constrain() on relations, Record::toArray(),
Collection::keyBy(), and pagination that renders through a template of your own.
Fixed
Writes are atomic and locked, a relation keeps the result its record came from,
count() no longer counts the first row twice, overlapping reads no longer cut
each other short, first() honours offset().
Performance
On 50 000 rows a sorted page holds the page, not the table — 0.6 MB against
33 MB, faster than the same work on plain fopen and fgetcsv. cursor() holds
one record at a time; inserts no longer collect the keys of the table, updates
and deletes no longer read the file twice.
Full changelog with before/after examples: CHANGELOG.md
Version 4.2.2
Version 4.2.1
в методы коллекций first,last,search,containts можно передать замыкание
Version 4.2.0
Добавлен метод filter, slice и pluck теперь возвращают коллекцию