【開発編Part3】半自動で運営される個人開発集サイト作ってみた

プログラミング

こんにちは!

今回は半自動で運営される個人開発集サイトシリーズの第5弾ということで、今回も進めていきます。

これまでのシリーズを並べてみましょう。

今回でシリーズは完結になります🎉

前回までは管理画面の記事一覧機能を作ったので、今回は手動で記事を登録・更新・削除できるようにしていきます。

また、最後にcronを使って記事の自動登録を実現しましょう。

記事登録

それでは、最初は記事の手動登録機能を追加していきましょう。

記事登録画面

resources/views/admin/articles/create.blade.phpを作成します。

@extends('layouts.admin')
@section('title', '管理側記事作成')

@section('style')
<style>
    .main {
        padding: 3rem 0;
        width: 90%;
        margin: 0 auto;
    }
    .form-group {
        margin-bottom: 20px;
    }
    .form-group label {
        display: block;
        margin-bottom: 5px;
    }
    .form-group input {
        width: 400px;
        padding: 10px;
        border: 1px solid #ccc;
        border-radius: 5px;
    }
    .submit-btn {
        width: 100px;
        padding: 10px;
        border: 1px solid #ccc;
        border-radius: 5px;
        background-color: #fff;
        cursor: pointer;
    }
</style>
@endsection

@section('content')
<div class="main">
    <h1>記事作成</h1>
    <form action="{{ route('admin.articles.store') }}" method="POST">
        @csrf
        <div class="form-group">
            <label for="title">タイトル</label>
            <input type="text" id="title" name="title">
        </div>
        <div class="form-group">
            <label for="url">URL</label>
            <input type="text" id="url" name="url">
        </div>
        <div class="form-group">
            <label for="author_name">著者名</label>
            <input type="text" id="author_name" name="author_name">
        </div>
        <div class="form-group">
            <label for="author_profile_image_url">著者プロフィール画像URL</label>
            <input type="text" id="author_profile_image_url" name="author_profile_image_url">
        </div>
        <div class="form-group">
            <label for="source">データ元</label>
            <input type="text" id="source" name="source">
        </div>
        <div class="form-group">
            <label for="article_created_at">公開日時</label>
            <input type="datetime-local" id="article_created_at" name="article_created_at">
        </div>
        <div class="form-group">
            <label for="article_updated_at">更新日時</label>
            <input type="datetime-local" id="article_updated_at" name="article_updated_at">
        </div>
        <div class="btn-group">
            <button class="submit-btn" type="submit">作成</button>
        </div>
    </form>
</div>
@endsection

さらに、記事一覧画面に登録画面へのリンクを設置していきましょう。

resources/views/lalyouts/admin.blade.phpを編集します。

<li><a href="{{ route('admin.articles.create') }}">記事作成</a></li>

ルーティング

routes/admin.phpを編集します。

// 管理側
Route::prefix('admin')->group(function () {
    ...
    Route::middleware('auth:admin_users')->group(function () {
        ...
        Route::get('/articles/create', 'App\\Http\\Controllers\\Admin\\ArticleController@create')->name('admin.articles.create');
        Route::post('/articles', 'App\\Http\\Controllers\\Admin\\ArticleController@store')->name('admin.articles.store');
    });
});

ここでは、登録画面のルートと登録処理のルートを設定しておきます。

コントローラ

それでは、コントローラにアクションを追加します。

app/Http/Controllers/Admin/ArticleController.phpを編集します。

public function create()
{
    return view('admin.articles.create');
}

public function store(Request $request)
{
    $data = [
        'title' => $request->title,
        'url' => $request->url,
        'source' => $request->source,
        'author_name' => $request->author_name,
        'author_profile_image_url' => $request->author_profile_image_url,
        'article_created_at' => $request->article_created_at,
        'article_updated_at' => $request->article_updated_at,
    ];
    $article = new Article();
    $article->fill($data)->save();
    return redirect()->route('admin.articles.index');
}

登録処理の際に、fill($data)というように保護を入れているため、Articleモデルにfillableが定義されていることを確認してください。

本来であれば、エラーハンドリングなどもっとしっかりと行なった方が良いところはたくさんありますが、今回は省略します。

これで記事登録機能は完了です。

記事更新

次に、記事の手動更新機能を追加していきます。

記事更新画面

resources/views/admin/articles/edit.blade.phpを作成します。

@extends('layouts.admin')
@section('title', '管理側記事編集')

@section('style')
<style>
    .main {
        padding: 3rem 0;
        width: 90%;
        margin: 0 auto;
    }
    .form-group {
        margin-bottom: 20px;
    }
    .form-group label {
        display: block;
        margin-bottom: 5px;
    }
    .form-group input {
        width: 400px;
        padding: 10px;
        border: 1px solid #ccc;
        border-radius: 5px;
    }
    .submit-btn {
        width: 100px;
        padding: 10px;
        border: 1px solid #ccc;
        border-radius: 5px;
        background-color: #fff;
        cursor: pointer;
    }
</style>
@endsection

@section('content')
<div class="main">
    <h1>記事編集</h1>
    <form action="{{ route('admin.articles.update', ['id' => $id]) }}" method="POST">
        @csrf
        @method('PUT')
        <div class="form-group">
            <label for="title">タイトル</label>
            <input type="text" id="title" name="title" value="{{ $title }}">
        </div>
        <div class="form-group">
            <label for="url">URL</label>
            <input type="text" id="url" name="url" value="{{ $url }}">
        </div>
        <div class="form-group">
            <label for="author_name">著者名</label>
            <input type="text" id="author_name" name="author_name" value="{{ $author_name }}">
        </div>
        <div class="form-group">
            <label for="author_profile_image_url">著者プロフィール画像URL</label>
            <input type="text" id="author_profile_image_url" name="author_profile_image_url" value="{{ $author_profile_image_url }}">
        </div>
        <div class="form-group">
            <label for="source">データ元</label>
            <input type="text" id="source" name="source" value="{{ $source }}">
        </div>
        <div class="form-group">
            <label for="article_created_at">公開日時</label>
            <input type="datetime-local" id="article_created_at" name="article_created_at" value="{{ $article_created_at }}">
        </div>
        <div class="form-group">
            <label for="article_updated_at">更新日時</label>
            <input type="datetime-local" id="article_updated_at" name="article_updated_at" value="{{ $article_updated_at }}">
        </div>
        <div class="btn-group">
            <button class="submit-btn" type="submit" onclick='return confirm("更新してよろしいですか?")'>更新</button>
        </div>
    </form>
</div>
@endsection

タスク一覧画面に更新画面へのリンクを設置します。

resources/views/admin/articles/index.blade.phpを編集します。

...
<th style="width: 5%;">編集</th>
...
<td>
    <div class="edit-btn">
        <a href="{{ route('admin.articles.edit', ['id' => $article->id]) }}">編集</a>
    </div>
</td>
...

ルーティング

routes/admin.phpにルートを追加します。

// 管理側
Route::prefix('admin')->group(function () {
    ...
    Route::middleware('auth:admin_users')->group(function () {
		    ...
        Route::get('/articles/edit/{id}', 'App\\Http\\Controllers\\Admin\\ArticleController@edit')->name('admin.articles.edit');
        Route::put('/articles/{id}', 'App\\Http\\Controllers\\Admin\\ArticleController@update')->name('admin.articles.update');
    });
});

コントローラ

コントローラにアクションを追加します。

app/Http/Controllers/Admin/ArticleController.phpを編集します。

public function edit($id)
{
    $article = Article::find($id);
    $data = [
        'id' => $article->id,
        'title' => $article->title,
        'url' => $article->url,
        'source' => $article->source,
        'author_name' => $article->author_name,
        'author_profile_image_url' => $article->author_profile_image_url,
        'article_created_at' => $article->article_created_at,
        'article_updated_at' => $article->article_updated_at
    ];
    return view('admin.articles.edit', $data);
}

public function update(Request $request, $id)
{
    $article = Article::find($id);
    $data = [
        'title' => $request->title,
        'url' => $request->url,
        'source' => $request->source,
        'author_name' => $request->author_name,
        'author_profile_image_url' => $request->author_profile_image_url,
        'article_created_at' => $request->article_created_at,
        'article_updated_at' => $request->article_updated_at
    ];
    $article->fill($data)->save();
    return redirect()->route('admin.articles.edit', ['id' => $id]);
}

引数で記事IDを受け取り、編集画面のレンダリングや更新処理などを行なっています。

これで記事更新機能も完了です。

記事削除

最後に、記事削除機能を追加していきます。

画面

まずはタスク一覧画面に削除リンクを設置します。

resources/views/admin/articles/index.blade.phpを編集します。

...
<th style="width: 5%;">削除</th>
...
<td>
    <div class="delete-btn">
        <form action="{{ route('admin.articles.delete', ['id' => $article->id]) }}" method="POST">
            @csrf
            @method('DELETE')
            <button type="submit" onclick='return confirm("本当に削除しますか?")'>削除</button>
        </form>
    </div>
</td>
...

ルーティング

routes/admin.phpに削除ルートを追加します。

Route::delete('/articles/{id}', 'App\\Http\\Controllers\\Admin\\ArticleController@delete')->name('admin.articles.delete');

コントローラ

コントローラにdeleteアクションを追加します。

public function delete($id)
{
    $article = Article::find($id);
    $article->delete();
    return redirect()->route('admin.articles.index');
}

削除後は記事一覧画面にリダイレクトするようにしておきます。

はい、これで記事削除機能も完了です。

データ収集の自動化

ここからは、Laravelのスケジューラという機能を使って、開発編Part1で作成した記事データ収集コマンドを自動で実行できるようにしていきます。

app/Console/Kernel.phpにコマンドを登録していきます。

protected function schedule(Schedule $schedule)
{
    $schedule->command('command:getNotePage')->dailyAt('8:00')->withoutOverlapping();
    $schedule->command('command:getQiitaPage')->dailyAt('8:00')->withoutOverlapping();
    $schedule->command('command:getZennPage')->dailyAt('8:00')->withoutOverlapping();
}

タスクスケジュールをどのような間隔で、どの時間に実行するかなどは自分で決めることができます。

これらの詳細については以下の公式サイトを参照してください。

Laravel 9.x タスクスケジュール

これでスケジューラへの登録も完了です。

動作確認

ローカル環境で動作確認する場合は、以下のように書き換えます。

protected function schedule(Schedule $schedule)
{
    $schedule->command('command:getNotePage')->everyThreeMinutes()->withoutOverlapping();
    $schedule->command('command:getQiitaPage')->everyThreeMinutes()->withoutOverlapping();
    $schedule->command('command:getZennPage')->everyThreeMinutes()->withoutOverlapping();
}

これで、コマンドが3分毎に実行されるようになるはずです(間違いがあったらごめんなさい、、、)

以下のコマンドでスケジューラを起動します。

php artisan schedule:work

本番環境での運用

本番環境では、以下のようにcronに登録すればOKです。

* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

はい、これでこのシリーズは完成です!お疲れ様でした!

まとめ

このシリーズは第5回目まで長々とお送りしてきましたが、いかがでしたでしょうか。

最後までお付き合いいただき、ありがとうございました。

これは筆者の個人開発プロジェクトの一つですが、個人開発はできるだけ小さく始めてすぐにリリースできるものの方がモチベーションが保ちやすいです。

小さいプロジェクトながらも、Laravelの基本的な機能のおさらいや本番運用など学ぶことは多かったです。

少しでもこちらの記事が参考になれば幸いです。

ではでは😊

コメント

タイトルとURLをコピーしました