Laravel Dusk and what browser tests are for

Laravel 5.4 came out on Tuesday, and the first thing I installed was Dusk. Browser tests without a Selenium server: it talks to ChromeDriver directly, the API is fluent, and a failed test leaves a screenshot behind. My first test was the login path: $this->browse(function ($browser) { $browser->visit('/login') ->type('email', 'user@example.com') ->type('password', 'secret') ->press('Log in') ->assertPathIs('/home'); }); It passed on the third run. The first run failed because the button on the test box had different text. The second failed because a JS animation was slower than the wait. This is the normal life of a browser test. It sees the real application, and the real application moves. ...

January 26, 2017 · 2 min · Murat Useinov

PHP 7.1: nullable is honest

// returns User or false, see wiki public function findByEmail($email) Every legacy codebase has this method. The comment is the type system. Half the callers check for false, some check for null because a sister method returns null, one caller checks nothing and works by luck. PHP 7.1 came out last week. Nullable types, void, iterable, multi-catch. Small features, but together they continue the direction 7.0 started: less implicit agreement, more signature. The one I care about is ?Type: ...

December 9, 2016 · 2 min · Murat Useinov

Order status is a graph

if ($order->status == 'paid') in the shipping module. Another one in the refund module. A slightly different one in the admin panel. Then someone cancels a shipped order from an admin button, because the button just writes the string, and the warehouse learns about it a week later. Symfony 3.2 is out this week and ships a Workflow component. Finally a framework says out loud what every order table has been whispering for years: status is a state machine, not a string column you assign wherever convenient. ...

November 30, 2016 · 2 min · Murat Useinov

The queue will run your job twice

The job called the payment provider. The call succeeded. Then the worker timed out before it marked the job done. The queue did what queues do: it retried. The provider did what it was asked: it charged again. The customer did what customers do and wrote an angry email. Nobody made a mistake here. The queue promises at-least-once delivery, and “at least” is written in the contract. Network partitions, worker crashes, deploy restarts. Sooner or later every job runs twice, and the jobs that hurt are exactly the ones with external effects: payments, emails, webhooks, API calls. ...

October 3, 2016 · 2 min · Murat Useinov

Parallel query in PostgreSQL 9.6

38 seconds to 11. Same query, same forty million rows, no index added, no SQL changed. The only difference is PostgreSQL 9.6, released yesterday, and one setting. The query is a typical report: count and sum over an events table, grouped by day, three months of data. On 9.5 the plan is one process grinding through the table. On 9.6 with parallelism enabled: Finalize HashAggregate -> Gather Workers Planned: 4 -> Partial HashAggregate -> Parallel Seq Scan on events Four workers scan their own chunks, aggregate partially, the leader merges. Just more hands. For years we optimized SQL as if the database had exactly one worker per query. That assumption expired yesterday. ...

September 30, 2016 · 2 min · Murat Useinov

Laravel 5.3: the app is not a website anymore

Mail::send() inside the checkout controller. A second copy inside the API controller. Then someone adds the chat webhook to one copy and forgets the other. Every project has this code. I have written it more than once. Laravel 5.3 came out this week, and the two big pieces, Notifications and Passport, both point the same way. The application is no longer a thing that renders HTML. It is a core that talks to browsers, mobile clients and third parties, and HTML is one of the outputs. ...

August 26, 2016 · 2 min · Murat Useinov

Column order in a composite index

Fifty rows. Three indexes on the table, one per column. Still slow. The query is the standard one from any multi-tenant application: SELECT * FROM orders WHERE tenant_id = ? AND status = ? ORDER BY created_at DESC LIMIT 50; “Add an index” is the typical reaction, and one index on each column is the typical result. The number of indexes is not the point. The order of columns inside one index is. ...

July 4, 2016 · 2 min · Murat Useinov

Doctrine and the 100 000 row import

Row sixty thousand. Allowed memory size exhausted. The import script on one project died there, and the code was the obvious loop: read a row, persist() an entity, next row, flush() at the end. On a hundred test rows it worked perfectly. It was my loop. The reason is the Unit of Work. Doctrine keeps every managed entity in memory, plus a snapshot of its original data for change tracking. Persist a hundred thousand entities and you hold a hundred thousand objects twice. This is not a bug. It is the price of the ORM’s main feature, and on a normal web request the price is invisible because the request dies young. ...

June 19, 2016 · 2 min · Murat Useinov

Symfony by the piece

A folder of cron scripts, each one a .php file with hand-parsed $argv. That is what CLI looked like on our legacy Kohana application until last month. One line fixed it: composer require symfony/console Now each script is a small Command class with named options, --help for free, and exit codes that cron can actually check. The framework around it did not notice anything. Console does not care who serves your HTTP. ...

May 10, 2016 · 2 min · Murat Useinov

Docker instead of a setup README

Two days. That is how long the new colleague spent last month getting one project to run on his laptop. The README is two pages: PHP with a specific set of extensions, Nginx, MySQL, Redis, and a paragraph that starts with “on OS X it is a bit different”. Every laptop in the team is a slightly different snowflake. So I finally tried Docker for local development. The compose file: ...

April 13, 2016 · 2 min · Murat Useinov