Articles with the tag Php
Extracting Wikilinks For Your Markdown Laravel Blog
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.
-
Tidying Tips -
[[Tidying Tips]]
-
It Works! -
[[Tidying Tips|It Works!]]
-
Wikilinks for the Win" -
[[#What is a Wikilink?|Wikilinks for the Win"]]
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.Different Ways To Pass Data To A Laravel View
#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.Better Http Status Codes In Laravel
'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.Counting Related Models In Laravel
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.Goodbye Forge, Hello Ploi
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.