Skip to content

2. A Post Model Example

Ross edited this page Sep 14, 2020 · 3 revisions

Initial config file

Lets take a look at an example config file and break down how it works.

$ php artisan scaffold:config Post 

After filling out the prompts, we are left with the following file at scaffolding/Post.php

<?php

return [
    'name' => 'Post',
    'order' => 0,
    'fields' => [
        'title' => ['type' => 'string', 'nullable' => false],
        'body' => ['type' => 'text', 'nullable' => false],
        'published_at' => ['type' => 'timestamp', 'nullable' => true],
        'is_published' => ['type' => 'boolean', 'nullable' => false],
    ],
    'relationships' => [
        'User' => ['type' => 'belongsTo'],
        'Comment' => ['type' => 'hasMany'],
        'Tag' => ['type' => 'belongsToMany'],
    ],
    'scaffolds' => [
        'model' => true,
        'migration' => true,
        'controller' => true,
        'resources' => true,
        'requests' => true,
        'policy' => true,
        'factory' => true,
    ],
];

Let's break this down by key:

name

The name of the model - will be the exact name you supplied to the command and used through the scaffolding


order

The order in which to publish the files - this is mostly due to migrations wanting to be run in a certain order. The script intentionally waits at least a second between models so that migrations do not have the same timestamp.


fields

The fields that the model contains. Additional info includes what the field type is as well as whether or not they can be nullable. This information is used for almost every file in the process including: attribute casting, adding the fields to the fillable array, request validation and resources, and model factories.


relationships

Relationships to other models.

If it is a through relationship, you may specify the other model through which it is related.

If it is a belongsToMany relationship and you know it will be many-to-many, you may add a pivot table. Only add the pivot table to ONE of the models, whichever is migrated second. The scaffolders are smart and will create these tables and relationships with the correct naming.

These relationships will be added to models, controllers, resources, and factories.


scaffolds

The scaffolds you want this process to create. Note that if you create a controller, you will also create a policy, requests, and resources.

Scaffolding the application

$ php artisan scaffold:application 

For now, this is the only file we have. I would recommend adding every model you know the application will have before running this command. Nevertheless, these are the files that are created based on the Post.php config above.

The model at app\Models\Post.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = ['title', 'body', 'published_at', 'is_published'];

    protected $casts = [
        'title' => 'string',
        'body' => 'string',
        'published_at' => 'timestamp',
        'is_published' => 'boolean',
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
}

The migration at database\migrations\<timestamp>_create_posts_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->string('title');
            $table->text('body');
            $table->timestamp('published_at')->nullable();
            $table->boolean('is_published')->default(0);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

The controller at app\Http\Controllers\PostController.php

<?php

namespace App\Http\Controllers;

use App\Http\Requests\PostStoreRequest;
use App\Http\Requests\PostUpdateRequest;
use App\Http\Resources\PostCollection;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\QueryBuilder;

class PostController extends Controller
{
    public function __construct()
    {
        $this->authorizeResource(Post::class);
    }

    public function index(Request $request)
    {
        $posts = QueryBuilder::for(Post::class)
            ->where('user_id', $request->user()->id)
            ->allowedFields(['id', 'title', 'body', 'published_at', 'is_published'])
            ->allowedIncludes(['user', 'comments', 'tags'])
            ->allowedFilters([
                AllowedFilter::exact('user_id'), 'title', 'body', 'published_at', AllowedFilter::exact('is_published'),
            ])
            ->allowedSorts(['title', 'body', 'published_at', 'is_published'])
            ->jsonPaginate();

        return new PostCollection($posts);
    }

    public function store(PostStoreRequest $request)
    {
        $post = new Post();

        $post->user()->associate($request->user());

        $post->fill($request->validated())->save();

        return (new PostResource($post))->response()->setStatusCode(Response::HTTP_CREATED);
    }

    public function show(Post $post)
    {
        return (new PostResource($post))->response()->setStatusCode(Response::HTTP_OK);
    }

    public function update(PostUpdateRequest $request, Post $post)
    {
        $post->update($request->validated());

        return (new PostResource($post))->response()->setStatusCode(Response::HTTP_OK);
    }

    public function destroy(Post $post)
    {
        $post->delete();

        return response(null, Response::HTTP_NO_CONTENT);
    }
}

A model resource at app\Http\Resources\PostResource.php

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
            'published_at' => $this->published_at,
            'is_published' => $this->is_published,
            'user' => new UserResource($this->whenLoaded('user')),
            'comments' => new CommentCollection($this->whenLoaded('comments')),
            'tags' => new TagCollection($this->whenLoaded('tags')),
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

A model resource collection at app\Http\Resources\PostCollection.php

<?php

namespace App\Http\Resources;

use App\Models\Post;
use Illuminate\Http\Resources\Json\ResourceCollection;

class PostCollection extends ResourceCollection
{
    public function toArray($request)
    {
        return $this->collection->transform(function (Post $post) {
            if ($post->relationLoaded('user')) {
               $post['user'] = new UserResource($post->user);
            }

            if ($post->relationLoaded('comments')) {
               $post['comments'] = new CommentCollection($post->comments);
            }

            if ($post->relationLoaded('tags')) {
               $post['tags'] = new TagCollection($post->tags);
            }

            return $post->getAttributes();
        });
    }
}

A store request at app\Http\Requests\PostStoreRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class PostStoreRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'title' => 'required|string',
            'body' => 'required|string',
            'published_at' => 'nullable',
            'is_published' => 'required|boolean',
        ];
    }
}

An update request at app\Http\Requests\PostUpdateRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class PostUpdateRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return ['title' => 'string', 'body' => 'string', 'published_at' => 'nullable', 'is_published' => 'boolean'];
    }
}

A policy at app\Policies\PostPolicy.php

<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Auth\Access\Response;

class PostPolicy
{
    use HandlesAuthorization;

    public function viewAny(?User $user)
    {
        return true;
    }

    public function view(?User $user, Post $post)
    {
        return true;
    }

    public function create(User $user)
    {
        return true;
    }

    public function update(User $user, Post $post)
    {
        return $user->is($post->user) ? Response::allow() : Response::deny('You do not own this post.');
    }

    public function delete(User $user, Post $post)
    {
        return $user->is($post->user) ? Response::allow() : Response::deny('You do not own this post.');
    }

    public function restore(User $user, Post $post)
    {
        return $user->is($post->user) ? Response::allow() : Response::deny('You do not own this post.');
    }

    public function forceDelete(User $user, Post $post)
    {
        return $user->is($post->user) ? Response::allow() : Response::deny('You do not own this post.');
    }
}

A factory at database\factories\PostFactory.php

<?php

namespace Database\Factories;

use App\Models\Comment;
use App\Models\Post;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    protected $model = Post::class;

    public function definition()
    {
        return [
            'user_id' => optional(User::inRandomOrder()->first())->id ?? User::factory(),
            'title' => $this->faker->sentence,
            'body' => $this->faker->paragraphs(5, true),
            'published_at' => $this->faker->boolean(50) ? $this->faker->dateTimeBetween($startDate = '-5 years', $endDate = 'now', 'UTC')->format('Y-m-d H:i:s') : null,
            'is_published' => $this->faker->boolean(50),
        ];
    }

    public function configure()
    {
        return $this->afterCreating(function (Post $post) {
            $post->comments()->saveMany(Comment::factory()->count(mt_rand(1, 10))->create());
        });
    }
}