arrilot/bitrix-models
A lightweight Eloquent-style ORM for Bitrix: map infoblocks and highload blocks to PHP model classes, query them through a fluent builder, and use familiar ActiveRecord patterns. It makes Bitrix data access cleaner and more maintainable for PHP projects.
Install via Composer alongside an existing Bitrix project:
composer require arrilot/bitrix-models
Look first at the package README and the src/Models directory. The four base classes you will live in are ElementModel, SectionModel, UserModel, and FileModel. Everything else (Query, relations, caching) is layered on top of those.
Minimal first use case — expose one infoblock as a model:
use Arrilot\BitrixModels\Models\ElementModel;
class News extends ElementModel
{
const IBLOCK_ID = 1;
// Whitelist the fields this model exposes.
public static function getMap(): array
{
return [
'ID' => 'ID',
'NAME' => 'NAME',
'ACTIVE' => 'ACTIVE',
'PREVIEW_TEXT' => 'PREVIEW_TEXT',
'DETAIL_TEXT' => 'DETAIL_TEXT',
];
}
}
Then query it like Eloquent:
$items = News::query()
->filter(['ACTIVE' => 'Y'])
->sort(['SORT' => 'ASC'])
->limit(10)
->get();
$one = News::find(42);
echo $one->NAME;
The mental model is: one model class per infoblock (or section/user/file pool), getMap() maps Bitrix field names to model attributes, and query() returns a builder that ultimately calls CIBlockElement::GetList (or the section/user equivalent). The package buys you object hydration, relations, and a fluent API; it does not replace Bitrix's storage, permissions, or cache components.
One model per entity, scopes for reusable filters. Encapsulate recurring filters as named builder methods so templates and components don't repeat raw filter arrays:
class News extends ElementModel
{
const IBLOCK_ID = 1;
public static function active()
{
return static::query()->filter(['ACTIVE' => 'Y']);
}
}
$news = News::active()->sort(['ACTIVE_FROM' => 'DESC'])->get();
Relations instead of hand-rolled GetList calls. Define hasOne/hasMany/belongsTo between models (e.g. an element belongs to a SectionModel, or references a UserModel or FileModel) and eager-load with with() when rendering lists. This is where the package earns its keep: without with(), each relation is a separate CIBlockElement::GetList, and a 50-item list becomes 50+ queries.
Model as the write boundary. Create and update through the model rather than calling CIBlockElement::Add/Update directly, so getMap() stays the single source of truth for which fields the app is allowed to touch:
$news = new News();
$news->NAME = 'Hello';
$news->ACTIVE = 'Y';
$news->save();
Cache at the query level and invalidate deliberately. Query caching (typically ->cache($seconds)) memoizes the underlying Bitrix call. Because the cache backend is usually Bitrix's own managed cache, you still need to clear it when content changes — hook model saves or use Bitrix's infoblock cache-clearing events, and don't assume the model invalidates for you.
Keep models thin, push display logic into accessors. Use accessors/mutators for derived attributes (image paths from FileModel, formatted dates, property arrays) so templates only see plain attributes. This is also where you normalize Bitrix's habit of returning arrays for what is conceptually a scalar.
Component/template integration. Instantiate models inside a component, pass hydrated collections into the template, and let the template call accessors. This keeps CIBlockElement::GetList parameter arrays out of .php templates, which is the main day-to-day win over raw Bitrix.
Mixed D7/legacy code. The models wrap the legacy CIBlock* API, so they interoperate fine with D7 code — but don't expect D7 EOrm entities to participate in with()/relation loading. Pick one style per module.
composer.json constraints before adding it to a PHP 8 project.filter() passes keys through to CIBlockElement::GetList, so ['>ID' => 100], [ '%NAME' => 'foo'], ['!ACTIVE' => 'N'] behave exactly as in Bitrix. This is powerful but undocumented in the package — learn Bitrix's filter syntax, not the package's.PROPERTY_* vs PROPERTY_*_VALUE, and multi-value properties returning arrays vs scalars, is a classic source of "works on one element, breaks on another" bugs. Normalize in accessors and test with both single- and multi-value properties.$element->section->NAME in a loop issues a query per element with no warning. Audit list-rendering code for missing with().CHECK_PERMISSIONS. The model runs as the current user unless you explicitly override it. Admin-panel testing can mask permission bugs that surface for anonymous users — verify with a real guest session.save() semantics. Know whether a save is an Add or an Update, which fields are written, and what happens to fields omitted from getMap() (they're typically untouched, but confirm — silent field loss is the worst failure mode here).$DB timing or Xdebug, and confirm the generated GetList parameters by temporarily dumping the query's toArray()/params rather than guessing.getMap(), the query class, or the base Model methods in your own abstract base model (e.g. AppModel extends ElementModel) rather than editing package files — this is the only sane way to survive a fork or patch.CIBlock* static calls; don't expect the package to make Bitrix unit-testable on its own.How can I help you explore Laravel packages today?