Search code examples
laravellaravel-9laravel-request

Validate IDs in array that are not in a related table


I have an endpoint that accepts and array of IDs in this format:

{
  "data": [
    {
      "id": 1
    },
    {
      "id": 2
    },
    {
      "id": 4
    }
  ]
}

...and in my validation requests file I have:

<?php

namespace AppNew\Http\Requests;

use Illuminate\Validation\Rule;
use Illuminate\Database\Query\Builder;
use Illuminate\Foundation\Http\FormRequest;

class ArchivePostsCollectionRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules(): array
    {
        return [
            'data' => [
                'array',
            ],
            'data.*' => [
                'required',
                'array',
            ],
            'data.*.id' => [
                'required',
                'integer',
                'distinct',
                Rule::exists('posts', 'posts.id')->where(function (Builder $query) {
                    return $query->where('posts.is_default', false);
                }),
            ],
        ];
    }

    public function messages(): array
    {
        return [
            'data.array' => 'Expected an array!',
            'data.*' => 'Expected an array of objects!',
            'data.*.id.required' => 'Expected an ID!',
            'data.*.id.integer' => 'Expected an integer for ID!',
            'data.*.id.integer' => 'Expected distinct IDs!',
            'data.*.id.exists' => 'Selected post is set to default and cannot be archived!',
        ];
    }
}

I want to add another rule to check that each post ID does not have comments; the comments table has a post_id column to check against. However I am stumped as to how to approach this from a Rule perspective!

Any pointers greatly appreciated.

Thanks, K...


Solution

  • You can do this in a couple different ways:

    'data.*.id' => [
        'required',
        'integer',
        'distinct',
        Rule::exists('posts', 'posts.id')->where(function (Builder $query) {
            return $query->where('posts.is_default', false);
        }),
        function (string $attribute, mixed $value, Closure $fail) {
            if (DB::table('comments')->where('post_id', $value)->count() !== 0) {
                $fail('Post has comments!');
            }
        },
    ],
    
    <?php
    
    namespace App\Rules;
     
    use Closure;
    use Illuminate\Contracts\Validation\ValidationRule;
    
    class PostWithoutCommentsRule implements ValidationRule
    {
        public function validate(string $attribute, mixed $value, Closure $fail): void
        {
            if (DB::table('comments')->where('post_id', $value)->count() !== 0) {
                $fail('Post has comments!');
            }
        }
    }
    
    'data.*.id' => [
        'required',
        'integer',
        'distinct',
        Rule::exists('posts', 'posts.id')->where(function (Builder $query) {
            return $query->where('posts.is_default', false);
        }),
        new PostWithoutCommentsRule,
    ],
    

    It really depends on how often you'll use it. If it's simple, I use closures. If it's complex, I use validator after. If it's something that's used more than once, no matter the complexity, I use custom Rules.