Loading...

Follow murze.be Blog - Freek Van der Herten on Feedspot

Continue with Google
Continue with Facebook
or

Valid

πŸ”₯ In @laravelphp you can customize validation error messages in a form request πŸ‘Œ pic.twitter.com/8XFUeuqFOT

β€” Freek Van der Herten πŸŽ† (@freekmurze) July 10, 2019
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 

πŸ”₯ Desktop always a mess? This little script tells macOS to not render desktop icons at all, so ~/Desktop is just another folder and your wallpaper is always unobstructed. pic.twitter.com/GjsmbrZ4c4

β€” Adam Wathan (@adamwathan) July 15, 2019
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 

Earlier this year we released v2 of laravel-event-sourcing. This package is probably the easiest way to getting started with event sourcing in Laravel. A significant feature of v2 was the addition of aggregates.

Today we released another new version of the package that adds test methods. These methods allow you to verify if the aggregate behaves correctly. In this post, I'll show you an example and explain how the test methods are implemented.

Testing an aggregate #

An aggregate is a class that decides to record events based on past events. To know more about their general purpose and the idea behind them, read this section on using aggregates to make decisions-based-on-the-past. In the remainder of the post, we're going to assume that you know how to work with aggregates.

Imagine you have an AccountAggregateRoot that handles adding and subtract an amount for a bank account. The account has a limit of -$5000.

use Spatie\EventProjector\AggregateRoot;

class AccountAggregateRoot extends AggregateRoot
{
    /** @var int */
    private $balance = 0;

    /** @var int */
    private $accountLimit = -5000;

    public function createAccount(string $name, string $userId)
    {
        $this->recordThat(new AccountCreated($name, $userId));

        return $this;
    }

    public function addMoney(int $amount)
    {
        $this->recordThat(new MoneyAdded($amount));

        return $this;
    }

    protected function applyMoneyAdded(MoneyAdded $event)
    {
        $this->balance += $event->amount;
    }

    public function subtractMoney(int $amount)
    {
        $this->hasSufficientFundsToSubtractAmount($amount)
            ? $this->recordThat(new AccountLimitHit($amount))
            : $this->recordThat(new MoneySubtracted($amount));
    }

    protected function applyMoneySubtracted(MoneySubtracted $event)
    {
        $this->balance -= $event->amount;
    }

    private function hasSufficientFundsToSubtractAmount(int $amount): bool
    {
        return $this->balance - $amount >= $this->accountLimit;
    }
}

Let's now test that rule that an account cannot go beyond its limit.

// in a PHPUnit test

/** @test */
public function it_can_subtract_money()
{
    AccountAggregateRoot::fake()
        ->given(new SubtractMoney(4999))
        ->when(function (AccountAggregate $accountAggregate) {
            $accountAggregate->subtractMoney(1);
        })
        ->assertRecorded(new MoneySubtracted(1))
        ->assertNotRecorded(AccountLimitHit::class);
}

/** @test */
public function it_will_not_make_subtractions_that_would_go_below_the_account_limit()
{
    AccountAggregateRoot::fake()
        ->given(new SubtractMoney(4999))
        ->when(function (AccountAggregate $accountAggregate) {
            $accountAggregate->subtractMoney(2);
        })
        ->assertRecorded(new AccountLimitHit(2))
        ->assertNotRecorded(MoneySubtracted::class);
}

You could write the above test a bit shorter. The given events can be passed to the fake method. You're also not required to use the when function.

/** @test */
public function it_will_not_make_subtractions_that_would_go_below_the_account_limit()
{
    AccountAggregateRoot::fake(new SubtractMoney(4999))
        ->subtractMoney(2)
        ->assertRecorded(new AccountLimitHit(2))
        ->assertNotRecorded(MoneySubtracted::class);
}
Implementing aggregate test methods #

Before starting implementing these test methods, I thought it was going to be a bit daunting. It turns out, it was not that hard.

To create an aggregate, you need to let a class extend our Spatie\EventProjector\AggregateRoot class.

class AccountAggregateRoot extends AggregateRoot {}

To keep the implementation of AggregateRoot clean, I wanted to avoid adding assertions on that class itself. The only method I added was fake. Calling this class will return a new FakeAggregateRoot instance in which the aggregate under test (in our example AccountAggregateRoot) is encapsulated.

Here is the implementation take from AggregateRoot.

/**
 * @param \Spatie\EventProjector\ShouldBeStored|\Spatie\EventProjector\ShouldBeStored[] $events
 *
 * @return $this
 */
public static function fake($events = []): FakeAggregateRoot
{
    $events = Arr::wrap($events);

    return (new FakeAggregateRoot(app(static::class)))->given($events);
}

In the FakeAggregateRoot all the test methods like given, when, assertRecorded and so on live.

Let's take a look at the simplest one: assertNothingRecorded.

public function assertNothingRecorded()
{
    PHPUnit\Framework\Assert::assertCount(0, $this->aggregateRoot->getRecordedEvents());
  
    return $this;
}

In the method above, we fetch the recorded events of the encapsulated aggregate root. A PHPUnit assertion is used to determine if that array is empty. If it is not, the PHPUnit test will fail.

If you're interested, you can take a look at the implementation of the other test methods.

In closing #

I hope you've enjoyed this little tour of the new test methods. Previously I've written two more posts about the implementation details of the package: here's how the aggregates are implemented and here's a post on how the developer experience was improved.

To know more about the package in general read the introductionary post or the documentation.

Be sure to also check out this big list of PHP & Laravel packages our team has made previously.

  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 
murze.be Blog - Freek Van der Herten by Freek Van Der Herten - 5d ago

Jason McCreary, creator of Laravel Shift, wrote a post mortem on a problem where too many mails were sent.

It was 7:07 am. I woke up to 56 emails, 17 tweets, 9 Slack messages, and 4 telegrams. All of which alerting me my SaSS product had sent 3,625 email messages to 1,544 users overnight. I am Jack's cold sweat. πŸ˜…

  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 

And if you're a @laravelphp user wondering how you can use this feature, you can easily extend the query builder to support this. πŸ₯³

Gist here: https://t.co/Wuavz0v7p9 pic.twitter.com/Vy6NJdJbcx

β€” Jonathan Reinink (@reinink) July 16, 2019
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 

The comment section of this blog used to be powered by Disqus. At its core, Disqus works pretty well. But I don't like the fact that it pulls in a lot of JavaScript to make it work. It's also not the prettiest UI.

I've recently replaced Disqus comments with webmentions. If you reply to, like or retweet this tweet, that interaction will, after a few minutes, appear in the comment section below.

Try it!

🌟 How to add webmentions to a Laravel powered bloghttps://t.co/S6KdTAW1rS#php #laravel #webmentions

β€” Freek Van der Herten πŸŽ† (@freekmurze) July 15, 2019

In this blog post, I'd like to explain why I moved to webmentions and how they are implemented on this blog.

What are webmentions? #

Recently my colleague Seb wrote a very interesting blog post on webmentions. Reading that post got me interested in webmentions. In the post, Seb already explains what webmentions are.

Webmentions are a protocol for websites to communicate across each other. What makes the webmention standard interesting is that it’s not tied to a single service β€” it’s a protocol. Webmentions can be aggregated from a range of different services from Twitter, to other blogs or even direct comments.

Sending webmentions #

Whenever I publish a blog post, a tweet with a link is automatically published. Ideally, I'd like to be notified by Twitter whenever that tweet (or any tweet that contains a link to my blog) is replied to, liked or retweeted.

Unfortunately, Twitter itself has no support for webmentions. But luckily some services can add this.

One of them is Bridgy. After it has been set up, Bridgy will scan Twitter for any tweets that contain a link to https://freek.dev. It'll also scan for new interactions (replies, retweets, likes) to those tweets.

Whenever it finds a link to my blog, it will look for a link tag on that page. If Bridgy finds this tag, it will send a webmention to the specified URL in href. More on that later.

<link  href="...">

Here's how Bridgy UI looks like.

It might not be the prettiest screen, but here you can see when it'll scan Twitter for new mentions and crawl my blog for new or updated webmention targets. Using the UI, you can manually start these actions. You can also resend a webmention. These options came in very handy when developing webmention support for this blog.

Receiving webmentions #

So Bridgy can send out webmentions, but we should also take care of receiving webmentions. Sure, you could code up a server yourself that can receive webmentions, but there are also specialized services for this. By using a service, you're sure that even when your server might be down for some reason, webmentions will still be recorded.

I'm using Webmention.io. It can filter out spammy webmentions performed by bots. To have Bridgy send all webmentions to webmention.io, I need to specify this href on each post page of my blog.

<link  href="https://webmention.io/freek.dev/webmention" />

On the webmention.io UI, you can see each webmention that Bridgy sent.

Webmention.io has support for sending out webhooks. We can configure it so that a soon as they get a webmention (and they deem it non-spammy), they send it to a specified URL.

With that out of the way, let's take a look at how we handle the incoming webhooks from webmention.ui in our Laravel app.

Processing webmentions #

A few weeks ago, my team at Spatie released a package called laravel-webhooks-client. This package makes it easy to handle any incoming webhook in a Laravel app.

After installing the package, the first thing that needs to be taken care of is making sure there's a URL that can accept the call made to our app by webmention.io. Using the package, you need to use the webhooks macro on Route. I've put this line in my routes file:

Route::webhooks('webhook-webmentions', 'webmentions');

The first parameter is the URL. The second parameter is the key used in the config file. Go over this section in the readme of the package on GitHub to know more about that.

In the screenshot above you've probably noticed that callback secret field. Webmention.io will add the value of that field in the payload it sends. I've also created a webmentions.webhook_secret entry in the config/services.php file and put that secret there.

When using the laravel-webhook-client package, you can specify a class that is responsible for determining if the webhook call is valid.

namespace App\Services\Webmentions;

use Illuminate\Http\Request;
use Spatie\WebhookClient\SignatureValidator\SignatureValidator;
use Spatie\WebhookClient\WebhookConfig;

class WebmentionWebhookSignatureValidator implements SignatureValidator
{
    public function isValid(Request $request, WebhookConfig $config): bool
    {
        if (! $request->has('secret')) {
            return false;
        }

        return $request->secret === config('services.webmentions.webhook_secret');
    }
}

Whenever webmention.io calls our app via the webhook, I'd like to transform the payload, associate it with the blog post it concerns, and add it to the database in the webmentions table.

All this is done in ProcessWebhookJob. Here is the code:

namespace App\Services\Webmentions

use App\Models\Post;
use App\Models\Webmention;
use Illuminate\Support\Arr;
use Spatie\Url\Url;
use Spatie\WebhookClient\ProcessWebhookJob as SpatieProcessWebhookJob;

class ProcessWebhookJob extends SpatieProcessWebhookJob
{
    public function handle()
    {
        $payload = $this->webhookCall->payload;

        if ($this->payloadHasBeenReceivedBefore($payload)) {
            return;
        }

        if (!$type = $this->getType($payload)) {
            return;
        }

        if (!$post = $this->getPost($payload)) {
            return;
        }

        Webmention::create([
            'post_id' => $post->id,
            'type' => $type,
            'webmention_id' => Arr::get($payload, 'post.wm-id'),
            'author_name' => Arr::get($payload, 'post.author.name'),
            'author_photo_url' => Arr::get($payload, 'post.author.photo'),
            'author_url' => Arr::get($payload, 'post.author.url'),
            'interaction_url' => Arr::get($payload, 'post.url'),
            'text' => Arr::get($payload, 'post.content.text'),
        ]);
    }
  
    private function payloadHasBeenReceivedBefore(array $payload): bool
    {
        $webmentionId = Arr::get($payload, 'post.wm-id');

        return Webmention::where('webmention_id', $webmentionId)->exists();
    }

    private function getType(array $payload): ?string
    {
        $types = [
            'in-reply-to' => Webmention::TYPE_REPLY,
            'like-of' => Webmention::TYPE_LIKE,
            'repost-of' => Webmention::TYPE_RETWEET,
        ];

        $wmProperty = Arr::get($payload, 'post.wm-property');

        if (!array_key_exists($wmProperty, $types)) {
            return null;
        }

        return $types[$wmProperty];
    }

    private function getPost(array $payload): ?Post
    {
        $url = Arr::get($payload, 'post.wm-target');

        if (!$url) {
            return null;
        }

        $postIdSlug = Url::fromString($url)->getSegment(1);

        [$id] = explode('-', $postIdSlug);

        return Post::find($id);
    }
}

I'm not going to go through this code step by step. Most of it should be self-explanatory.

Now that we have stored the webmentions in the database and they are associated with a post, we can loop through them in a Blade view.

Using webmentions as a commenting system vs Disqus #

I have a love-hate relationship with Disqus. The core features work very well, there's support for nested comments, logging in possible to post comments is possible via a variety of systems.

On the downside, the UI Disqus adds to a site is ugly (imho) and can't be customized too much. Because of how it is rendered on the page, Google won't index the comments. Disqus can also inject ads into the comments.

Using Twitter webmentions as a comment system is also a mixed bag. It's nice to that you're in full control of how the comments are rendered. You'll also store the comments somewhere yourself, so you're not dependent on a service that keeps the content for you.

On the flip side, you must keep in mind that webmentions don't appear immediately. Commenters need an account of another service (in my case they need to have a Twitter account). Comments can't be long, because there's a low limit on the number of characters a tweet can have (you can of course still use multiple tweets. There's no support for nested tweets whatsoever.

Right now I think I like the trade-offs of webmentions more. I don't have too many comments on the posts on this blog, but most of my audience seems to be active on Twitter. Maybe my thoughts on this will change in the future. We'll see...

Closing thoughts #

I hope you've enjoyed this tour of how webmentions are implemented on this blog. Here are some resources to check out if you want to know more:

What do you think of this? Let me know in the comments below πŸ˜€.

  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 
murze.be Blog - Freek Van der Herten by Freek Van Der Herten - 1w ago
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 

Matthew Rocklin reminds us that sometimes you shouldn't extract code.

However, there is also a cost to this behavior. When a new reader encounters this code, they need to jump between many function definitions in many files. This non-linear reading process requires more mental focus than reading linear code.

  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 

Nicolas Beauvais wrote a nice blogpost on how he went about adding support for internationalization to his app.

When working on web applications, translation strings are typically stored in the backend of your app while they’re mostly used in the frontend. This is the main challenge because you need to communicate the translations from your backend to your frontend code.

  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 

At the Oh Dear blog, my colleague Mattias explains how to use our service to verify that your site is still online after a deploy.

You can use our API to trigger an on demand run of both the uptime check and the broken links checker. If you add this to, say, your deploy script, you can have near-instant validation that your deploy succeeded and didn't break any links & pages.

Read for later

Articles marked as Favorite are saved for later viewing.
close
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 

Separate tags by commas
To access this feature, please upgrade your account.
Start your free month
Free Preview