| name | onoffice-adapter |
| description | Query and manage onOffice CRM data (estates, addresses, activities, relations, files) with the innobrain/laravel-onoffice-adapter fluent query builder, including batching and testing with fakes. |
onOffice Adapter Development
When to use this skill
Use this skill when working with the innobrain/laravel-onoffice-adapter package — whenever code needs to talk to the onOffice (enterprise) real estate CRM API: querying or modifying estates, addresses, activities, relations, files, or writing tests for such code.
Core concepts
- Every resource has a Facade repository in
Innobrain\OnOfficeAdapter\Facades\* (e.g. EstateRepository, AddressRepository, ActivityRepository, RelationRepository, FileRepository, SearchCriteriaRepository, FieldRepository, AppointmentRepository, TaskRepository, UserRepository).
Repository::query() returns an Eloquent-like builder: select(), where(), whereIn(), whereLike(), whereBetween(), orderBy(), orderByDesc(), limit(), offset(), when().
- Terminal methods:
get() (auto-paginates all pages), first(), find($id), count(), each(fn) (chunked processing), create(), modify(). Special-purpose builders (relations, files, fields) support only a subset — e.g. the relation builder has no select()/where()/orderBy()/find()/count().
- Always
select() only the fields you need to minimize API payload.
- For endpoints without a repository, use
BaseRepository::query()->call(new OnOfficeRequest(...)).
Querying
use Innobrain\OnOfficeAdapter\Facades\EstateRepository;
$estates = EstateRepository::query()
->select(['Id', 'objekttitel', 'kaufpreis', 'wohnflaeche', 'status'])
->where('status', 1)
->where('kaufpreis', '<', 500000)
->whereIn('objektart', ['haus', 'wohnung'])
->orderByDesc('kaufpreis')
->limit(50)
->get();
$estate = EstateRepository::query()->find(123);
For large datasets, process in chunks instead of loading everything:
EstateRepository::query()->each(function (array $estates) {
});
Writing data
$estate = EstateRepository::query()->create([
'objektart' => 'haus',
'vermarktungsart' => 'kauf',
'kaufpreis' => 350000,
]);
EstateRepository::query()
->addModify(['kaufpreis' => 320000])
->modify(123);
ActivityRepository::query()
->estateId(41)
->addressIds([34])
->create([
'actionkind' => 'Email',
'actiontype' => 'Ausgang',
'note' => 'Contract sent',
]);
Relations
Link records (buyer ↔ estate, owner ↔ estate, etc.) with RelationRepository and the OnOfficeRelationType enum:
use Innobrain\OnOfficeAdapter\Enums\OnOfficeRelationType;
use Innobrain\OnOfficeAdapter\Facades\RelationRepository;
$contacts = RelationRepository::query()
->relationType(OnOfficeRelationType::ContactPersonAll)
->parentIds([48])
->get();
RelationRepository::query()
->relationType(OnOfficeRelationType::Buyer)
->parentIds([48])
->childIds([181])
->create();
Files
use Innobrain\OnOfficeAdapter\Facades\FileRepository;
FileRepository::upload()
->uploadInBlocks()
->saveAndLink(base64_encode($fileContent), [
'module' => 'estate',
'relatedRecordId' => 409,
'file' => 'document.pdf',
'Art' => 'Dokument',
]);
$pictures = EstateRepository::pictures(123)
->category(['Titelbild', 'Foto'])
->get();
Batching multiple actions in one HTTP call
Use the Query facade to bundle builders (or raw OnOfficeRequest objects) into a single API call. Batched actions are never paginated — you get the first page only (max 500 records per action). Only builders implementing toRequest() are batchable: estate, address, activity, appointment, task, user, last-seen, relation, and link builders. Others (files, fields, search criteria, ...) throw.
use Innobrain\OnOfficeAdapter\Facades\Query;
$results = Query::batch([
EstateRepository::query()->select('kaufpreis')->limit(10),
AddressRepository::query()->whereLike('Vorname', 'Max'),
])->once();
$estates = data_get($results[0], 'data.records');
$results = Query::batch([
EstateRepository::query()->withId(5),
AddressRepository::query()->withId(9),
])->once();
Testing
Never hit the real API in tests. Fake repositories with response pages built from record factories:
use Innobrain\OnOfficeAdapter\Facades\EstateRepository;
use Innobrain\OnOfficeAdapter\Facades\Testing\RecordFactories\EstateFactory;
EstateRepository::fake(EstateRepository::response([
EstateRepository::page(recordFactories: [
EstateFactory::make()->id(1)->data(['kaufpreis' => 450000]),
]),
]));
$estates = EstateRepository::query()->get();
expect($estates)->toHaveCount(1);
EstateRepository::assertSentCount(1);
EstateRepository::assertSent(fn ($request) => true);
- Multiple
page() entries in one response() fake pagination; multiple response() entries fake sequential calls.
Repository::preventStrayRequests() makes unstubbed requests throw.
- Batches are faked through
Query::fake() only — per-repository fakes are never consumed by a batch. Fake exactly one page per batched action.
- Factories exist for each record type (
EstateFactory, AddressFactory, ActivityFactory, RelationFactory, FileFactory, ...).
Useful extras
EstateRepository::query()
->when($maxPrice, fn ($q) => $q->where('kaufpreis', '<=', $maxPrice))
->get();
EstateRepository::query()
->before(fn ($request) => Log::info('Sending', ['request' => $request->toArray()]))
->after(fn ($response) => Log::info('Received', ['status' => $response->status()]))
->get();
EstateRepository::query()->dump()->get();
EstateRepository::query()->dd()->get();
EstateRepository::query()->withCredentials($token, $secret)->get();
Field names
onOffice field names are customer-specific and mostly German (kaufpreis, wohnflaeche, objektart). When unsure which fields exist or which values a select field permits, query them via FieldRepository instead of guessing:
use Innobrain\OnOfficeAdapter\Facades\FieldRepository;
$fields = FieldRepository::query()->withModules(['estate'])->get();