Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Four Tochki Orders Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: it is a Symfony bundle, not a Laravel package

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:

  1. Out-of-process: run the bundle inside a small Symfony console app (or inside your existing Symfony service) and talk to it from Laravel over a queue/HTTP boundary.
  2. In-process bridge: pull it in via Composer, boot a dedicated Doctrine 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.

Minimal steps

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:

  1. vendor/baks-dev/four-tochki-orders/composer.json — required PHP version, baks-dev/* siblings, autoload namespace.
  2. Resources/config/ — service definitions, entity mappings (*.orm.xml or attributes), routes.
  3. The domain folder (Entity/, Domain/, UseCase/ or whatever this module uses) — find the aggregate root for an order.
  4. Migrations/ — the actual schema; this tells you more than any README would.
  5. 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.

Migrations: do not diff against your Laravel database

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.

First use case: read-only

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.

Implementation Patterns

Pattern 1 — Anti-corruption layer (recommended default)

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.

Pattern 2 — Dedicated EntityManager

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.

Pattern 3 — Out-of-process boundary (lowest risk)

Run the Symfony-side logic in its own container/CLI process and talk to it from Laravel:

  • Laravel dispatches SyncFourTochkiOrders → Redis/SQS.
  • A worker shells out to php /srv/four-tochki/bin/console app:orders:sync --json.
  • Output is written to an agreed JSON contract, Laravel upserts it.

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.

Pattern 4 — Scheduled sync into an Eloquent read model

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.

Pattern 5 — Events

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;
}

Pattern 6 — Testing against the interface

You get three test seams for free from Pattern 1:

  • Unit/browser tests: FakeFourTochkiOrders returns fixtures. No Doctrine, no PHP 8.4 problem.
  • Contract tests: a separate @group four-tochki-orders suite that runs the real implementation against a test DB, if you can bootstrap the Symfony kernel in CI.
  • Queued job tests: Bus::fake() / Queue::fake() on the boundary, plus assertDispatched(FourTochkiOrderCreated::class).

Never make your feature tests hit the real 4tochki API.

Gotchas and Tips

The PHP 8.4 requirement is a hard wall

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.

Documentation risk is the real risk

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:

  • Every class name in this document is illustrative — the Service\OrderService style snippet that circulates in assessments is not verified. Grep the vendor directory before you write a use statement.
  • Pin exactly: "baks-dev/four-tochki-orders": "7.4.*", and commit composer.lock.
  • Vendor the source or use composer patches for local fixes, so a composer update doesn't silently undo your work.
  • Audit transitive baks-dev/* and symfony/* constraints — a bundle that requires symfony/framework-bundle can drag your Symfony components to incompatible versions.

Doctrine vs Eloquent coexistence

  • One EntityManager per metadata configuration. Sharing managers across app/Models and the vendor entity set is how you get ghost schema diffs.
  • Never doctrine:schema:update --force in a Laravel app.
  • Doctrine proxies need a writable directory — in a read-only container this fails with a confusing metadata exception. Point setProxyDir at storage/framework/doctrine.
  • Doctrine returns strings for DECIMAL and dates as DateTimeImmutable. Map money to integer kopecks at the DTO boundary, not in your Blade templates.

Russian-marketplace specifics

  • Domain language and error messages will be Russian. Wrap exceptions: anything Symfony-shaped becomes a Laravel-shaped exception in the adapter, or your Handler renders a 500 with a Symfony stack trace.
  • Amounts are RUB, timezone MSK. Store UTC + integer minor units; convert at the edges.
  • Tires/wheels orders carry SKU + dimension attributes (width/profile/diameter) — expect nested value objects. Don't flatten them into your 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.

Operational guardrails

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

Extension points and the fork decision

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.

Debugging checklist when something silently does nothing

  1. Is the service actually registered? php artisan tinker --execute="dump(app(FourTochkiOrders::class));"
  2. Did migrations run against the four-tochki connection, not the default one? php artisan db:show --connection=four_tochki
  3. Is the entity manager pointed at the right metadata paths? Dump $em->getMetadataFactory()->getAllMetadata() && count(...).
  4. Is it a queue worker running stale code? Restart workers after deploy — a serialised DTO with a renamed property fails only inside the worker.
  5. Are you looking at a cached query/metadata result? Flush the Doctrine caches in local, not just Laravel's.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource