Articles with the tag Laravel

Extracting Wikilinks For Your Markdown Laravel Blog

1 month ago Tue, Aug 22, 2023

I recently rebuilt this site so I could write my articles in Markdown. I'm used to writing in Obsidian and have come to rely heavily on wikilinks. Laravel comes with Markdown baked in. Behind the scenes, it makes use of a package called league/commonmark. Commonmark offers a large list of extensions, but as we will see, none of them work to add Wikilinks functionality.

TL;DR #Create your own wikilinks custom delimiter

#What is a Wikilink?

First things first, let's just do a quick recap. What exactly is a wikilink? Created and used by Wikipedia, wikilinks are a convenient way to link to internal pages.

Say we have an internal article called "Tidying Tips", which it just so happens we do, then the conventional way to link to it would be by using an HTML a tag:

<a href="https://www.carlcassar.com/articles/tidying-tips">
	Tidying Tips
</a>

In Markdown, this can be done by using markdown's link syntax:

[Tidying Tips](https://www.carlcassar.com/articles/tidying-tips)

Now, its not unusual to link to several other internal pages within one article and links tend to be quite cumbersome to type out by hand. All the slashes and w's make it all too easy to make a mistake. Wikilinks were created as a wrapper to make it quick and convenient to link to other internal content. Using our earlier example, the syntax for a wikilink is as follows:

[[Tidying Tips]]

It's as simple as that. You just wrap the title of the page you are linking to in double square brackets, [[ and ]].

Wikilinks come with some additional features. We can change the "looks like" or alt text of the link by adding our preferred text after the title:

[[Tidying Tips|an short article on refactoring]]

Additionally, we can link to an id on the same page by using a # symbol.

[[#Some Heading On The Page]]

#League/Commonmark and Wikilinks

Commonmark is a fantastic package that handles a lot of work when it comes to using markdown in Laravel and PHP. Unfortunately, it does not support Wikilinks and will simply ignore any text wrapped in two square brackets.

Looking down the list of extensions, you will find one called Mentions. The mentions extension allows one to parse mentions like @carlcassar and #2343. Unfortunately, it seems that although it will parse the prefixes @, # and almost anything else that I experimented with, including digits (3...) and random letters (s...), it will ignore square brackets, even when they are escaped correctly in a regular expression.

#Create your own wikilinks custom delimiter

Handily, not all is lost. Commonmark exposes its delimiter processor API.

Delimiter processors allow you to implementย delimiter runsย the same way the core library implements emphasis.

Using a delimiter processor, we can process text that is encapsulated in between a number of matching symbols, e.g. *example*, {example}, {{example}}.

First, we must add a delimiter processor to the Commonmark Enviroment:

$environment->addDelimiterProcessor(new WikilinksDelimiterProcessor());

Next, we must create the WikilinksDelimiterProcessor which must implement the DelimiterProcessorInterface.

class WikilinksDelimiterProcessor implements DelimiterProcessorInterface  
{
	//
}

In order to satisfy the contract of the Interface, we must implement 5 methods:

public function getOpeningCharacter(): string  
{  
    return '[';  
}  
  
public function getClosingCharacter(): string  
{  
    return ']';  
}  
  
public function getMinLength(): int  
{  
    return 2;  
}  
  
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int  
{  
    return 2;  
}

public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void  
{
	// What now?
}

In our case, the opening character is [, the closing character is ] and we require two of each in order to process the literal string contained in our delimiter.

All that's left is to implement the process function which will tell commonmark what to do when it encounters our wikilink.

First, we need to get the literal string from the AbstractStringContainer $opener.

Now this is where it gets a little bit tricky. It seems that commonmark really doesn't like working with double square brackets. In this case, although it correctly identifies the string we are looking for, it fails to remove the second opening bracket and we are left with the literal string [Tidying Tips.

No problem, we can simply remove the bracket ourselves:

private function getLiteralFrom(AbstractStringContainer $opener): Stringable  
{  
    $literal = $opener->next()->getLiteral();  
  
    // Commonmark does not work for double square brackets,  
    // so we will remove a leftover square bracket from    
    // the beginning of the opener literal string.    
    return Str::of($literal)->substr(1);  
}

At this point, we should keep in mind our eventual goal - to replace the text with a link. Commonmark has our back and provides a link node, which requires a url parameter and accepts an optional label and title. With this in mind, lets create a function to get those attributes from the literal:

$attributes = $this->getAttributes(  
    $this->getLiteralFrom($opener)  
);
private function getAttributes(Stringable $literal): Collection  
{  
    $explodedLiteral = $literal->explode('|');  
  
    $wikiTitle = $explodedLiteral[0];  
    $wikiLooksLike = count($explodedLiteral) > 1 ? $explodedLiteral[1] : null;  
  
    return collect([  
        'url' => $this->getUrlFor($wikiTitle),  
        'label' => $wikiLooksLike ?? $wikiTitle,  
        'title' => $wikiLooksLike ?? $wikiTitle,  
    ]);}

Finally, we need to cater for hash links as well as ordinary links:

private function getUrlFor(string $wikiTitle)  
{  
    $slug = Str::of($wikiTitle)->slug();  
  
    return $this->isHashLink($wikiTitle) ? "#$slug" : $slug;  
}  
  
private function isHashLink(string $wikiTitle): bool  
{  
    return Str::of($wikiTitle)->substr(0, 1) == '#';  
}

Here is the final WikilinksDelimiterProcessor class:

<?php  
  
namespace App\Console\Commands\Support;  
  
use Illuminate\Support\Collection;  
use Illuminate\Support\Stringable;  
use League\CommonMark\Delimiter\DelimiterInterface;  
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;  
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;  
use League\CommonMark\Node\Inline\AbstractStringContainer;  
use Str;  
  
class WikilinksDelimiterProcessor implements DelimiterProcessorInterface  
{  
    public function getOpeningCharacter(): string  
    {  
        return '[';  
    }  
    
    public function getClosingCharacter(): string  
    {  
        return ']';  
    }  
    
    public function getMinLength(): int  
    {  
        return 2;  
    }  
    
    public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int  
    {  
        return 2;  
    }  
    
    public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void  
    {  
        $attributes = $this->getAttributes(  
            $this->getLiteralFrom($opener)  
        );  
        
        $opener->next()->replaceWith(new Link(  
            $attributes->get('url'),  
            $attributes->get('label'),  
            $attributes->get('title')  
        ));    
	}
	  
    private function getLiteralFrom(AbstractStringContainer $opener): Stringable  
    {  
        $literal = $opener->next()->getLiteral();  
  
        // Commonmark does not work for double square brackets,  
		// so we will remove a leftover square bracket from   
		// the beginning of the opener literal string.    
	    return Str::of($literal)->substr(1);  
    }  
    
    private function getAttributes(Stringable $literal): Collection  
    {  
        $explodedLiteral = $literal->explode('|');  
  
        $wikiTitle = $explodedLiteral[0];  
        $wikiLooksLike = count($explodedLiteral) > 1 ? $explodedLiteral[1] : null;  
  
        return collect([  
            'url' => $this->getUrlFor($wikiTitle),  
            'label' => $wikiLooksLike ?? $wikiTitle,  
            'title' => $wikiLooksLike ?? $wikiTitle,  
        ]);    
	}  
        
    private function getUrlFor(string $wikiTitle)  
    {        
	    $slug = Str::of($wikiTitle)->slug();  
  
        return $this->isHashLink($wikiTitle) ? "#$slug" : $slug;  
    }  
    
    private function isHashLink(string $wikiTitle): bool  
    {  
        return Str::of($wikiTitle)->substr(0, 1) == '#';  
    }
}

#Test it out

All being well, I will include a few links using wikilinks syntax and you should be able to click through to each of them.

Thank you for reading this article.

If you've made it this far, you might like to connect with me on ๐• where I post similar content and interact with like-minded people. If this article was helpful to you I'd really appreciate it if you would consider buying me a coffee.
Continue Reading

Different Ways To Pass Data To A Laravel View

1 year ago Mon, Jan 10, 2022

#1. Using a magic method

First up, Laravel uses some PHP magic to make sense of fluent methods. If, for example, you have an array of people in a variable $people, then you can use a magic method withPeople on the view() helper function (or View:: facade) to pass the array to your view. In your blade file, your people array will be available via a $people variable.

Route::get('/', function () {
    $people = ['Bob', 'John', 'Simon'];

    return view('welcome')->withPeople($people);
});

This method makes your code more readable to humans which will minimise the time it takes another developer (or your future self) to make sense of this code. Unfortunately, your IDE will most likely not be able to offer code completion or Intellisense for the withPeople method since it is using magic methods and has not been declared on the Illuminate\View\Factory class.

#2. Using a string parameter

If you liked the readability of the first method, but don't want to deal with IDE warnings, you can pass your array through with a string key that Laravel will use as the name for the variable made available in your view.

For example, if you have a people array, you can pass it to the view by using the with method, passing the string key people as the first argument and the array $people as the second argument.

Route::get('/', function () {
    $people = ['Bob', 'John', 'Simon'];

	return view('welcome')->with('people', $people);
});

This method has a slight limitation in that you are left with a magic string, ie: people that must be kept up to date. Say, for example, that you rename $people to $names using your IDE. It might not be immediately obvious that you need to change the string people to names. For this reason, string values that are used to create variables are often a source of confusion and code drift over time.

#3. Using an array

If you have more than one variable that needs to be passed to the view, then you can pass an array as the second attribute to the view helper method (or View Facade).

Route::get('/', function () {
    $people = ['Bob', 'John', 'Simon'];
    $days = ['Monday' 'Tuesday'];

    return view('welcome', [
	      'people' => $people
	      'days' => $days
	  ];
});

This method can make your code look clean and concise, especially if you inline your variables.

Route::get('/', function () {
    return view('welcome', [
	      'people' => ['Bob', 'John', 'Simon'];
	      'days' => ['Monday' 'Tuesday'];
	  ];
});

As with previous methods, you still have to set a string key for the array. Once again, string keys are often problematic, because they have no inherent meaning in PHP. This means that your editor will not be able to assist you with renaming this key across many locations.

#4. Using the compact method

Finally, we can make use of a function built in to PHP that can automatically create an array containing variables and their values.

If you are not familiar with the compact method, you can read about it in the PHP documentation.

Route::get('/', function () {
    $people = ['Bob', 'John', 'Simon'];
    $days = ['Monday' 'Tuesday'];

    return view('welcome', compact('people', 'days'));
});

This method is useful if you want to pass an array through to the view, but don't want to have to make an array with a key that is the same as the value variable.

// compact('people') === ['people' => $people]

Because compact is a well-defined PHP function, your editor may be able to deduce that the string key refers to a variable name in the same code block. compact can be extremely useful if you have many variables to pass through to a view and don't want to pass them inline.

compact('var1', 'var2', etc...)

#Conclusion

There is no right or wrong way to pass data to a Laravel View. Some people detest magic methods, whilst others dislike string keys. The most important thing is that you choose a method that feels comfortable to you and try to be consistent across your code.

Let me know which method you prefer in the comments.

Thank you for reading this article.

If you've made it this far, you might like to connect with me on ๐• where I post similar content and interact with like-minded people. If this article was helpful to you I'd really appreciate it if you would consider buying me a coffee.
Continue Reading

Better Http Status Codes In Laravel

1 year ago Sun, Jan 2, 2022

'Magic numbers' like 200 or 401 can cause a lot of confusion for colleagues or your future self. It's not always immediately obvious what these numbers represent.

A magic number is a number in the code that has no context or meaning.

Luckily, when it comes to HTTP Status Codes, we can make use of a complete set of constants that will make the meaning of your code self evident.

return Response::HTTP_OK;

For example, Response::HTTP_OK will return 200, Response::HTTP_UNAUTHORIZED will return 401 and my personal favourite Response::HTTP_I_AM_A_TEAPOT will return 418.

This is possible because the Illuminate\Http\Response class extends the Symfony\Component\HttpFoundation\Response class.

Finally, we also have access to an array of all status codes via Response::$statusTexts. This is handy if you want to list, validate or otherwise iterate over all status codes.

Thank you for reading this article.

If you've made it this far, you might like to connect with me on ๐• where I post similar content and interact with like-minded people. If this article was helpful to you I'd really appreciate it if you would consider buying me a coffee.
Continue Reading

Counting Related Models In Laravel

1 year ago Sun, Jan 2, 2022

Very often when retrieving a model in Laravel, it is useful to load a count of related models at the same time. For example, when loading a blog post, you might want to display the number of comments left on that post.

Luckily, Laravel has a method to do just that:

$posts = Post::withCount('comments')->get();

What's more, as of Laravel 8, you can also make use of the withMin, withMax, withAvg, withSum, and withExists methods.

Read more in the Laravel Documentation.

Thank you for reading this article.

If you've made it this far, you might like to connect with me on ๐• where I post similar content and interact with like-minded people. If this article was helpful to you I'd really appreciate it if you would consider buying me a coffee.
Continue Reading

Goodbye Forge, Hello Ploi

2 years ago Thu, May 6, 2021

When it first launched, Laravel Forge was the easiest and cheapest way to deploy a Laravel application onto a custom server. Recently, after repeatedly being presented with upgrade banners for new features, I decided to investigate the alternatives before committing to a higher fee. Happily, I stumbled upon Ploi.

At first I was skeptical that anyone could compete with the Laravel Core team who manage the established and stable product that is Laravel Forge, but over a few short hours I was increasingly convinced that Ploi was not only a suitable alternative, but hands down the better offering. Here's what finally changed my mind, made me switch all my sites and end my Forge subscription.

When it first launched, Laravel Forge was priced at $10 a month for unlimited servers, sites and services. This was an amazing and probably somewhat underpriced offering. It was incredible to be able to launch a Laravel site with a few clicks and forget about server management entirely. No more digging around in Nginx config files and hours spent installing the latest version of this or that version of PHP. Over time, more features were added for team functionality. I didn't upgrade because I didn't need access to these features. Later, the basic subscription price was increased slightly for new users, but the fee for users on existing plans remained the same.

In the last year or so, however, certain new features such as server monitoring and database backups were added but only made available to users on a premium tier at $39 a month. This is quite a steep increase for features that have now become commonplace on other services. Nevertheless, this is a price that would be well worth paying if it werent for two simple reasons.

First, instead of being hidden in the settings, these additional features are presented to all users irrespective of the tier they were on. This means that as you explore the UI, you are continually presented with useless pages for features you cannot use and encouraged somewhat relentlessly to upgrade to a higher tier.

Second, a lot has changed in the few years since Laravel Forge was launched. It is now much easier to deploy sites directly to cloud providers such as AWS and Digital Ocean. Furthermore, there are a number of competitors that offer the same feature set and more for a fraction of the price.

After a quick comparison of the alternatives, I stumbled upon Ploi. It makes a great first impression and gets better and better from there. First, its fresh design makes for a welcome break from Forge's simplistic user interface which despite being improved over the years has started to feel a bit tired. Its features are well organised, searchable and accessible and it's help pages are useful and comprehensive. Next, its pricing is cheaper than Forge on every tier and offers all Forge features for less than half the price. It is intuitive and easy to use and guides you to make full use of the featurews available to you. Even better, there is no mention of upgrading unless you are looking to do so.

Finally, when you are ready to pay a little bit more, you gain access to a rich array of features which are simply not available on Laravel Forge, such as:

  • Site Monitoring
  • File Browsing
  • Status Pages
  • The ability to suspend sites with the click of a button
  • Zero Downtime deployment

All of this combined, made it a no brainer for me. Much as Laravel Forge has served me well over recent years, it was time to move on. I hope that this increased competition will make both products better and can't wait for all the quality of life goodies that we can look forward to over the next few years.

If you're convinced or simply want to try Ploi for yourself, you can sign up for a free trial with no credit card required.

Thank you for reading this article.

If you've made it this far, you might like to connect with me on ๐• where I post similar content and interact with like-minded people. If this article was helpful to you I'd really appreciate it if you would consider buying me a coffee.
Continue Reading