20
Creating a Laravel Artisan command to import posts from DEV via the command line
In a previous part of our series, we saw how to install and configure TailwindCSS 3.0 within a Laravel project and how to set up a basic blog layout for our demo application. With that base template in place, the fourth part of the series is focused on the back-end side of the application.
In this post, you'll learn how to create an Artisan command in Laravel to import posts from DEV using their API and how to save the content as markdown files in your local application storage folder.
Before moving along, make sure you have followed all steps in the first tutorial of the series in order to bootstrap your Laravel application and development environment. This will require you to have Docker, Curl, and Git installed on your system.
If you haven't yet, bring your environment up with:
./vendor/bin/sail up -d
This command will start up your environment and keep it running in the background. Your Laravel application should now be available at http://localhost
from your browser.
This tutorial assumes that you have an alias to ./vendor/bin/sail
on the root of the application folder, so that you can run Sail with ./sail
. You can create such an alias with the following commands, executed from the application root folder:
ln -s ./vendor/bin/sail sail
chmod +x sail
Start by creating a new Artisan command using the make:command
utility. This will create a new file with boilerplate code for an Artisan command, saved at app/Console/Commands/
.
./sail artisan make:command ImportPosts
Open the newly generated file app/Console/Commands/ImportPosts.php
in your code editor, and you'll see content like this:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class ImportPosts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'command:name';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
return 0;
}
}
Start by changing the command:name
for the command signature you'd like to use. In this demo we'll use import:dev
.
The handle()
method is where the command action happens; this is what will be executed when you run artisan import:dev
from the command line.
Luckily for us, DEV has an API that we can query to obtain the latest posts from a user, without the need to use an authorization token, since that is all public data. The endpoint we want to query is:
https://dev.to/api/articles?username=DEV_USERNAME
To make the request, we'll use minicli/curly, which is a small PHP library based on curl
. Require that dependency with:
./sail composer require minicli/curly
Once you have the dependency installed, add a use
directive at the top of the file so that you can easily reference Minicli\Curly\Client
in your code. We'll also use Laravel's Carbon/Carbon
library to parse the article's publish date, so you should include that within your use
directives as well. Lastly, include the Illuminate\Support\Facades\Storage
class with another use
directive, since we'll be using this facade class to check and save files within the local file storage system.
This is how the first lines of your app/Console/Commands/ImportDev.php
file should look like:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Minicli\Curly\Client;
use Illuminate\Support\Facades\Storage;
use Carbon\Carbon;
Next, update your handle()
method with the following code:
public function handle()
{
$crawler = new Client();
$headers = [
'User-Agent: curly 0.1',
];
$articles_response = $crawler->get('https://dev.to/api/articles?username=DEV_USERNAME&per_page=10', $headers);
if ($articles_response['code'] !== 200) {
$this->error('Error while contacting the dev.to API.');
return Command::FAILURE;
}
$articles = json_decode($articles_response['body'], true);
foreach ($articles as $article) {
$article_slug = $article['slug'];
$published = new Carbon($article['published_at']);
$filename = $published->format('YmdH') . '-' . $article_slug .'.md';
if (Storage::disk('local')->exists($filename)) {
continue;
}
$endpoint = sprintf('https://dev.to/api/articles/%s', $article['id']);
$article_query = $crawler->get($endpoint, $headers);
if ($article_query['code'] == 200) {
$article_full = json_decode($article_query['body'], true);
Storage::disk('local')->put($filename, $article_full['body_markdown']);
$this->info("Article saved: " . $filename);
}
}
return Command::SUCCESS;
}
This code will make a request to the DEV API to obtain a list with the latest 10 articles from a user. To get the full contents of the article, including the article's metadata defined in the front matter, we'll make a second request once we check that the article hasn't been imported before. Then, we'll use the Storage
utility to save the body_markdown
of each article as a markdown file in the application's storage folder. The files are saved using a combination of the article's publish date and the article slug as filename, with the .md
extension.
Don't forget to change DEV_USERNAME to your own username on DEV.to.
Save the file when you're finished.
With the command ready, you can now run it with:
./sail artisan import:dev
Article saved: 2021121515-setting-up-tailwindcss-30-on-a-laravel-project-1cb8.md
Article saved: 2021121013-creating-a-new-laravel-application-with-sail-and-docker-no-php-required-4c2n.md
Article saved: 2021120318-how-to-set-up-elgatos-stream-deck-on-ubuntu-linux-2110-pdh.md
Article saved: 2021112011-dynamic-twitter-header-images-with-dynacover-github-action-35nb.md
Article saved: 2021111818-how-to-build-github-actions-in-php-with-minicli-and-docker-1k6m.md
Article saved: 2021111618-automatically-update-your-contributors-file-with-this-github-action-workflow-d98.md
Article saved: 2021102717-10-sourcegraph-search-tricks-for-open-source-contributors-and-maintainers-44n9.md
Article saved: 2021092715-3d-design-creating-printable-solid-shapes-from-svg-files-with-inkscape-and-freecad-266e.md
Article saved: 2021090615-3d-design-with-freecad-part-2-working-with-sketcher-part-design-workbenches-3leo.md
Article saved: 2021090614-an-introduction-to-3d-design-with-freecad-part-1-navigation-3gjo.md
You should see the titles of your latest posts showing up in the output of the command. Once the command is finished running, have a look at your storage/app
folder and you'll see your posts as individual markdown files named after each post's publish date and slug.
ls storage/app
2021090614-an-introduction-to-3d-design-with-freecad-part-1-navigation-3gjo.md
2021090615-3d-design-with-freecad-part-2-working-with-sketcher-part-design-workbenches-3leo.md
2021092715-3d-design-creating-printable-solid-shapes-from-svg-files-with-inkscape-and-freecad-266e.md
2021102717-10-sourcegraph-search-tricks-for-open-source-contributors-and-maintainers-44n9.md
2021111618-automatically-update-your-contributors-file-with-this-github-action-workflow-d98.md
2021111818-how-to-build-github-actions-in-php-with-minicli-and-docker-1k6m.md
2021112011-dynamic-twitter-header-images-with-dynacover-github-action-35nb.md
2021120318-how-to-set-up-elgatos-stream-deck-on-ubuntu-linux-2110-pdh.md
2021121013-creating-a-new-laravel-application-with-sail-and-docker-no-php-required-4c2n.md
2021121515-setting-up-tailwindcss-30-on-a-laravel-project-1cb8.md
public
In this article, which is part 3 out of 4 in our Getting started with Laravel series, we've seen how to create an Artisan command and how to import posts from a user of DEV.to using the DEV API and the minicli/curly
library. The command uses the underlying storage system from Laravel to store the posts as individual markdown files in your storage/app
folder.
In the next part of this series, we'll finish this demo application by updating the index controller and listing the posts on the main application's page.
You can follow Sourcegraph on Twitch to be notified when we go live. Follow our YouTube channel for the full recap videos of our livestreams.
20