Mon Nov 1
I took the weekend to work through how to add a name
column to the scaffolding generated by mix phx.gen.auth
to more closely match Laravel Jetstream's profile update flow. In Jetstream, the photo, name, and email address are part of the same change request.
For gen.auth
, email changes are gated behind a validation request happening first. This makes sense, you want to make sure the email is valid and accessible before you change the user to it. The problem with this is that lumping the name into the same changset creates a slight code smell.
To carve out saving name involves somewhat of a side effect as we modify email_changeset
in lib/opportunity_knocks/accounts/user.ex
to allow the pattern %{changes: %{name: _}} = changeset -> changeset |> delete_change(:email)
in the case statement. This allows name to go through but we push email out of the change in the event both name and email are updated at the same time. This lets us change apply_user_email
in lib/opportunity_knocks/accounts.ex
to use the snippet:
|> case do
%{changes: %{name: _}} = changeset -> changeset |> Repo.update()
%{changes: %{email: _}} = changeset -> changeset |> Ecto.Changeset.apply_action(:update)
This is where the smell is particularly strong as I'm doing the same pattern matching to allow the change in the case of name
and ignore it for email
. I believe as I gain more Elixir knowledge I may refactor this but I also know how deviation can be due to my exposure to Laravel Shift. Shift uses a series of patches to perform framework upgrades. Files that are not altered get changed more easily and when a deviation is detected (merge conflict), Shift goes with its version so you are consistently applying your changes on top of the now improved version. This is the approach we would all take given time or time machines.
Tue Dec 1
Forgot to transfer environment variables for the following:
QUEUE_CONNECTION=redis
REDIS_DB=0
REDIS_CACHE_DB=1
REDIS_HORIZON_DB=2
0
, 1
, and 2
are the defaults this really didn't impact anything.QUEUE_CONNECTION
defaulted to synchronized jobs instead of via redis so Horizon had nothing to do.flushdb
reset horizon and now running jobs via dispatch()
show up as expected.Tue Nov 17
Research into keeping the latest x rows
You cant delete with a skip/take (some mysql limitation)
DELETE FROM `apiLogs`
WHERE id <= (
SELECT id
FROM (
SELECT id
FROM `apiLogs`
ORDER BY id DESC
LIMIT 1 OFFSET 42 -- keep this many records
) foo
)
Ended up going with a more generic version as rather than start from now we can just start at zero and delete up to the count we're looking for.
SELECT
`id`
FROM
`apiLogs`
WHERE
`application_id` NOT in('SAGESYNC', 'SAGESYNCMS', 'SAGESYNCXC4')
ORDER BY
`created_at` ASC
LIMIT 3500
Mon Nov 9
My first attempt was to have an array of application_id but this made the Collection->where() usage more difficult.
{
"blocklist": [
{
"application_id": ["SAGESYNC", "SAGESYNCMS"],
"status": ["422"]
},
{
"application_id": ["SAGESYNCXC4"]
}
]
}
Ultimately we had to settle on this form as the where() clause looks for values.
{
"blocklist": [
{
"application_id": "SAGESYNC",
"status": ["422"]
},
{
"application_id": "SAGESYNCMS",
"status": ["422"]
},
{
"application_id": "SAGESYNCXC4"
}
]
}
/**
* Filter columns against configured blocklist.
*
* @param string|null $application_id
* @param string|null $method
* @param string|null $uri
* @param string|null $status
* @return void
*/
public function isBlocked($application_id, $method, $uri, $status)
{
$result = false;
$blocklist = collect($this->getBlocklist());
$blocked = $blocklist->where('application_id', $application_id);
$blocked->each(function ($block) use (&$result, $method, $uri, $status) {
$methods = Arr::get($block, 'method', null);
$uris = Arr::get($block, 'uri', null);
$statuses = Arr::get($block, 'status', null);
$filtered = $this->hasValue($methods, $method) ?? $this->hasValue($uris, $uri) ?? $this->hasValue($statuses, $status);
if (is_null($filtered) || $filtered === true) {
// $filtered is true when a value is found or null when all fields are null.
$result = true;
}
});
return $result;
}
/**
* An array has value (has normally checks for keys not values).
*
* @param array|null $array
* @param mixed $value
* @return boolean
*/
private function hasValue($array, $value)
{
if (is_null($array)) {
return null;
}
return in_array($value, $array);
}
/**
* Retrieve blocklist section from config file.
*
* @return null|array
*/
private function getBlocklist()
{
$localDisk = \Storage::disk('local');
$contents = $localDisk->get("data/apiLogs/config.json");
$json = json_decode($contents, true);
if (!is_null($json)) {
return $json['blocklist'];
}
return null;
}
Mon Nov 2
See https://laracasts.com/discuss/channels/laravel/the-process-has-been-signaled-with-signal-9
valet restart
Fri Nov 6
The docker image and my local .tool-versions for asdf no longer work.
asdf erlang make[4]: *** [obj/x86_64-apple-darwin19.6.0/opt/smp/inet_drv.o] Error 1
export CFLAGS="-O2 -g -fno-stack-check -Wno-error=implicit-function-declaration"
asdf install erlang 23.0.3
Wed Mar 25
MariaDB generated JSON columns are possible if we unpack the syntax.
// $table->string('payload_type')->storedAs('payload->>"$.type"')->after('name');
$table->string('payload_type')->storedAs("JSON_UNQUOTE(JSON_EXTRACT(`payload`, '$.type'))")->after('name');
Thu Feb 13
Query a group of records based on parameters deep in a parent relation.
$files = \App\Models\File::whereHas('material', function(\Illuminate\Database\Eloquent\Builder $material) {
$material->whereHas('category', function(\Illuminate\Database\Eloquent\Builder $category) {
$category->whereHas('bucket', function(\Illuminate\Database\Eloquent\Builder $bucket) {
$bucket->where('path', 'vbs-2020-focus');
});
});
})->toSql();
Mon Feb 3
Mocking multiple overloads based on arguments.
$this->mock('overload:Stripe\Charge', function ($mock) {
$mock->shouldReceive('create')
->with(\Mockery::on(function ($argument) {
return is_array($argument) && isset($argument['source']) && $argument['source'] == 'card_expired';
}))
->times(1)
->andThrow(new \Stripe\Error\InvalidRequest("Source should be a valid card id", null)) // This relies on the try/catch logic in the Job
->getMock()
->shouldReceive('create')
->times(1)
->andReturn($this->getTestStripeResponse()); // Overload our instance with this default
});
We ignore our raised exception by you guessed it, a try/catch block.
try {
$this->withoutExceptionHandling()->artisan("subscriptions:process-remaining --day={$day} --no-progress")
->expectsOutput('4 subscription(s) found');
} catch(\Throwable $exception) {
}
\Event::assertDispatched(\App\Events\CardCharged::class, 3);
try {
$this->withoutExceptionHandling()->artisan("subscriptions:process-remaining --day={$day} --no-progress")
->expectsOutput('1 subscription(s) found');
} catch(\Throwable $exception) {
}
Wed Feb 2
Illuminate\Queue\MaxAttemptsExceededException
This stack trace shows the method call set at 5 maxTries:
Illuminate\Queue\MaxAttemptsExceededException: App\Jobs\PendingOrderTransferJob has been attempted too many times or run too long. The job may have previously timed out. in /home/forge/obr.rethinkgroup.org/vendor/laravel/framework/src/Illuminate/Queue/Worker.php:394
Stack trace:
#0 /home/forge/obr.rethinkgroup.org/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(314): Illuminate\Queue\Worker->markJobAsFailedIfAlreadyExceedsMaxAttempts('redis:horizon', Object(Illuminate\Queue\Jobs\RedisJob), 5)
The failed job payload shows 5 attempts were made:
{"type":"job","timeout":null,"tags":["App\\Model\\PendingOrderStatus:1534"],"id":"3078","data":{"command":"O:32:\"App\\Jobs\\PendingOrderTransferJob\":8:{s:6:\"status\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":3:{s:5:\"class\";s:28:\"App\\Model\\PendingOrderStatus\";s:2:\"id\";i:1534;s:10:\"connection\";s:5:\"mysql\";}s:6:\"\u0000*\u0000job\";N;s:10:\"connection\";s:13:\"redis:horizon\";s:5:\"queue\";s:15:\"orders:transfer\";s:15:\"chainConnection\";N;s:10:\"chainQueue\";N;s:5:\"delay\";N;s:7:\"chained\";a:0:{}}","commandName":"App\\Jobs\\PendingOrderTransferJob"},"displayName":"App\\Jobs\\PendingOrderTransferJob","timeoutAt":null,"pushedAt":"1580851619.0033","maxTries":null,"job":"Illuminate\\Queue\\CallQueuedHandler@call","attempts":5}
Mon Dec 16
Reminder of JSON fields
$table->json('source_payload');
$table->string('order_id')->storedAs('source_payload->>"$.eCommerceOrderId"');
$table->string('user_id')->storedAs('source_payload->>"$.eCommerceCustomerId"');
$table->string('organization_id')->storedAs('source_payload->>"$.obrorgid"');
// See https://themsaid.com/laravel-mysql-json-colum-fast-lookup-20160709/
$table->index('order_id');
$table->index('user_id');
$table->index('organization_id');
SELECT
*, payload->>"$[0].eCommerceCustomerId" as `eCommerceCustomerId`
FROM
pendingOrderStatus
WHERE
payload->>"$[0].eCommerceCustomerId" = '16350';
Mon Nov 18
name
isn't a publicly accessible property facepalm.name
is allowed oddly enough but sorting isn't respected so my guess is Laravel can't find the property and tries to sort by the object instead.It's extremely off-putting to see it partially sort and have no fucking clue why it didn't fully sort as expected. I spent longer than I should've and hated myself when I found the answer.
return $categories->sortBy(function(\App\Classes\Uploads\Category $category) {
return $category->getName();
}, SORT_NATURAL);