baks-dev/four-tochki-orders
Orders module for the 4tochki integration in BaksDev: install via Composer, publish assets with baks:assets:install, then run Doctrine migrations. PHP 8.4+, Symfony console, MIT licensed.
The README is the entire documentation, and everything in it points away from Laravel:
php bin/console ... — Laravel ships artisan, not a Symfony bin/console.baks:assets:install — a Symfony/Baks console command.doctrine:migrations:diff / doctrine:migrations:migrate — Doctrine ORM migrations, not Eloquent.php bin/phpunit --group=four-tochki-orders — a PHPUnit group inside a Symfony kernel test suite.Baks-dev modules assume a Symfony application with Doctrine ORM, a Baks core kernel, and Twig assets. So "day-to-day in Laravel" realistically means one of two things:
EntityManager for its entities only, and call its services from a thin Laravel adapter.Pick (1) if you value your sanity; pick (2) only if you can afford to own the Doctrine bridge.
composer require baks-dev/four-tochki-orders
Expect symfony/framework-bundle, symfony/console, doctrine/orm, doctrine/migrations, and other baks-dev/* packages to come along. Check what you actually got before doing anything else:
composer show baks-dev/four-tochki-orders
ls vendor/baks-dev/four-tochki-orders
Then read the source, in this order — this is your real documentation:
vendor/baks-dev/four-tochki-orders/composer.json — required PHP version, baks-dev/* siblings, autoload namespace.Resources/config/ — service definitions, entity mappings (*.orm.xml or attributes), routes.Entity/, Domain/, UseCase/ or whatever this module uses) — find the aggregate root for an order.Migrations/ — the actual schema; this tells you more than any README would.Tests/ — the four-tochki-orders group is the closest thing to a spec you will get.Do not run baks:assets:install blindly at first. It copies files into a Symfony public/ path and can overwrite things in a Laravel app. Read the command class first.
This is the single most dangerous command in the README:
php bin/console doctrine:migrations:diff
Run that against your application's existing database and Doctrine will happily generate DROP TABLE statements for every Eloquent table it does not know about. If you must use Doctrine migrations, scope them hard:
// config/doctrine_migrations.php (standalone Doctrine setup)
return [
'table_storage' => ['table_name' => 'doctrine_migration_versions'],
'migrations_paths' => [
'FourTochki\\Migrations' => base_path('vendor/baks-dev/four-tochki-orders/Migrations'),
],
'connection' => $fourTochkiConnection, // dedicated, not your default
];
…and always with a schema_filter on the entity manager so Doctrine only sees the four_tochki_* tables. Back up the database and run --dry-run equivalents first.
For most teams the better trade-off is to read the bundle's migration files once and hand-write Laravel migrations that mirror them. You get artisan migrate, rollback, and no Doctrine runtime.
Start with a read-only Artisan command that boots the module and lists orders. No writes, no webhooks, no money movement. Prove you can see the data before you try to create it.
// app/Console/Commands/FourTochkiOrdersProbe.php
class FourTochkiOrdersProbe extends Command
{
protected $signature = 'four-tochki:probe {--limit=10}';
public function handle(FourTochkiOrders $orders): int
{
foreach ($orders->latest((int) $this->option('limit')) as $order) {
$this->line(json_encode($order->toArray(), JSON_UNESCAPED_UNICODE));
}
return self::SUCCESS;
}
}
FourTochkiOrders is an interface you write (see below). The concrete implementation is the only file that ever uses a BaksDev\... class — that isolation is the whole point.
One interface, one concrete implementation, one fake. Nothing else in your codebase knows the package exists.
// app/Services/FourTochki/FourTochkiOrders.php
interface FourTochkiOrders
{
/** @return OrderData[] */
public function latest(int $limit): array;
public function find(string $externalId): ?OrderData;
public function create(CreateOrderData $data): string; // returns external id
}
// app/Services/FourTochki/DoctrineFourTochkiOrders.php
final class DoctrineFourTochkiOrders implements FourTochkiOrders
{
public function __construct(private EntityManagerInterface $em) {}
public function latest(int $limit): array
{
// Namespace/class verified by hand from vendor/baks-dev/.../Entity
$repo = $this->em->getRepository(\BaksDev\FourTochkiOrders\Entity\Order::class);
return array_map(
fn ($o) => OrderData::fromEntity($o),
$repo->findBy([], ['createdAt' => 'DESC'], $limit),
);
}
}
// app/Providers/FourTochkiServiceProvider.php
public function register(): void
{
$this->app->singleton(FourTochkiOrders::class, fn ($app) => new DoctrineFourTochkiOrders(
$app->make('four-tochki.doctrine.em'),
));
if ($this->app->environment('testing')) {
$this->app->bind(FourTochkiOrders::class, FakeFourTochkiOrders::class);
}
}
Doctrine entities must never cross the boundary. Map them to plain DTOs / Eloquent read models immediately. Serialising a Doctrine entity into a queued job will fail on proxy classes.
Give the module its own connection and manager so its mapping metadata cannot interfere with anything else:
// config/four-tochki.php
return [
'connection' => env('FOUR_TOCHKI_DB', 'four_tochki'),
'prefix' => 'four_tochki_',
];
// app/Providers/FourTochkiDoctrineServiceProvider.php
$paths = [base_path('vendor/baks-dev/four-tochki-orders')];
$config = Setup::createAttributeMetadataConfiguration($paths, app()->isLocal());
$config->setMetadataCache(CacheAdapter::wrap(app('cache.store')));
$config->setQueryCache(CacheAdapter::wrap(app('cache.store')));
$em = EntityManager::create(config('database.connections.four_tochki'), $config);
$this->app->instance('four-tochki.doctrine.em', $em);
Only worth it if you genuinely need the bundle's write-side services. If you only read orders, skip Doctrine entirely and mirror the tables.
Run the Symfony-side logic in its own container/CLI process and talk to it from Laravel:
SyncFourTochkiOrders → Redis/SQS.php /srv/four-tochki/bin/console app:orders:sync --json.Upside: PHP 8.4+ and Doctrine stay quarantined in their own image; you can upgrade or abandon the module without touching your app. Downside: an extra deployable.
This is the pattern most teams actually end up shipping:
// app/Jobs/SyncFourTochkiOrders.php
class SyncFourTochkiOrders implements ShouldQueue, ShouldBeUnique
{
public int $tries = 3;
public array $backoff = [30, 120, 600];
public function handle(FourTochkiOrders $orders): void
{
foreach ($orders->since(now()->subHours(2)) as $dto) {
FourTochkiOrder::updateOrCreate(
['external_id' => $dto->id],
[
'status' => $dto->status,
'total_kopecks' => $dto->totalKopecks,
'payload' => $dto->raw,
'synced_at' => now(),
],
);
}
}
}
Then schedule it with overlap protection:
$schedule->job(new SyncFourTochkiOrders)->everyTenMinutes()->withoutOverlapping();
updateOrCreate on a unique external_id index is your idempotency guarantee. Assume the module's own idempotency handling is unknown until proven otherwise.
If the bundle dispatches Symfony events, do not let Laravel listeners subscribe to Symfony's dispatcher. Translate at the adapter and re-dispatch Laravel events after DB::commit():
public function create(CreateOrderData $data): string
{
$id = $this->delegate->create($data);
DB::afterCommit(fn () => event(new FourTochkiOrderCreated($id)));
return $id;
}
You get three test seams for free from Pattern 1:
FakeFourTochkiOrders returns fixtures. No Doctrine, no PHP 8.4 problem.@group four-tochki-orders suite that runs the real implementation against a test DB, if you can bootstrap the Symfony kernel in CI.Bus::fake() / Queue::fake() on the boundary, plus assertDispatched(FourTochkiOrderCreated::class).Never make your feature tests hit the real 4tochki API.
php 8.4+ is declared by the package. A Laravel 10/11 app on PHP 8.2/8.3 cannot install it without composer install --ignore-platform-reqs (do not do this in CI) or pinning:
"config": { "platform": { "php": "8.4.0" } }
Treat this as an upgrade project, not a composer line. If you use the out-of-process pattern, the 8.4 requirement evaporates.
0 stars, 0 dependents, release notes that say nothing but v7.4.3, MIT license, last release 2026. There is no issue tracker history to learn from. Practical consequences:
Service\OrderService style snippet that circulates in assessments is not verified. Grep the vendor directory before you write a use statement."baks-dev/four-tochki-orders": "7.4.*", and commit composer.lock.composer patches for local fixes, so a composer update doesn't silently undo your work.baks-dev/* and symfony/* constraints — a bundle that requires symfony/framework-bundle can drag your Symfony components to incompatible versions.EntityManager per metadata configuration. Sharing managers across app/Models and the vendor entity set is how you get ghost schema diffs.doctrine:schema:update --force in a Laravel app.setProxyDir at storage/framework/doctrine.DECIMAL and dates as DateTimeImmutable. Map money to integer kopecks at the DTO boundary, not in your Blade templates.Handler renders a 500 with a Symfony stack trace.orders table prematurely; keep a payload JSON column for the raw shape while the mapping stabilises.baks:assets:install writes to a Symfony public/ path. If any of the module's frontend touches your Laravel public/, review the command before running it in production.| Risk | Guardrail |
|---|---|
| API downtime mid-sync | $tries + $backoff, failed_jobs review, dead-letter alert |
| Duplicate order creation | unique index on external_id, ShouldBeUnique job |
| Rate limiting | RateLimiter::for('four-tochki'), ->middleware('throttle:four-tochki') on the job |
| Webhook replay / forged callbacks | HMAC check + Cache::add() on the callback id before processing |
| Silent failures | Log the outbound request id and the module's returned id at info; log payload at debug only (PII) |
| Long sync blocking deploys | chunked pagination, Bus::batch(), never load all orders into memory |
Prefer, in order: configuration → decorating the interface → events → a small composer patches diff → a hard fork. Fork only if you need to change the Doctrine mapping or the API client itself, and if you do, keep the fork in your own namespace and drop the baks-dev package — you will be maintaining it alone.
php artisan tinker --execute="dump(app(FourTochkiOrders::class));"php artisan db:show --connection=four_tochki$em->getMetadataFactory()->getAllMetadata() && count(...).How can I help you explore Laravel packages today?