Secure Uploads - v1.0.0

Custom Validators

FileValidationService is bound to FileValidatorInterface in the service provider. To extend or replace the pipeline, build your own implementation and rebind it.

Add checks via a subclass

The lightest-touch option — keep the shipped behaviour and add your own:

namespace App\Services;

use ArtisanPackUI\SecureUploads\FileUpload\ValidationResult;
use ArtisanPackUI\SecureUploads\Services\FileValidationService;
use Illuminate\Http\UploadedFile;

class TenantAwareFileValidator extends FileValidationService
{
    public function validate(UploadedFile $file, array $options = []): ValidationResult
    {
        $result = parent::validate($file, $options);

        if ($this->wouldExceedTenantQuota($file)) {
            $result->addError('Upload would exceed your storage quota.');
        }

        return $result;
    }
}

Bind in a service provider's register():

$this->app->singleton(FileValidatorInterface::class, TenantAwareFileValidator::class);

Your subclass is now used everywhere — the trait, the middleware, the storage service all resolve from the container.

Replace the pipeline entirely

Implement FileValidatorInterface from scratch when the shipped pipeline doesn't fit:

namespace App\Services;

use ArtisanPackUI\SecureUploads\Contracts\FileValidatorInterface;
use ArtisanPackUI\SecureUploads\FileUpload\ValidationResult;
use Illuminate\Http\UploadedFile;

class StrictPdfValidator implements FileValidatorInterface
{
    public function validate(UploadedFile $file, array $options = []): ValidationResult
    {
        $result = new ValidationResult($file);

        if ($file->getClientMimeType() !== 'application/pdf') {
            $result->addError('Only PDF uploads accepted on this endpoint.');
        }

        if ($file->getSize() > 50 * 1024 * 1024) {
            $result->addError('PDF too large (max 50 MB).');
        }

        // ... your checks
        return $result;
    }

    // Implement the rest of the interface ...
}

Most apps don't need this — the shipped service covers the common cases and is configurable. Subclassing is almost always the right choice.

Per-endpoint overrides

For endpoint-specific rules without rebinding, pass $options to validate() or attachSecureFile():

$post->attachSecureFile($file, [
    'maxFileSize' => 100 * 1024 * 1024,
    'allowedMimeTypes' => ['video/mp4'],
]);

Use this for single-endpoint policy variation; reserve rebinding for cross-cutting changes that should apply everywhere.

Building rules instead

If you need a one-off check that only applies in a specific Form Request, write a normal Laravel rule rather than subclassing the validator:

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Http\UploadedFile;

class PdfHasNoEmbeddedFonts implements Rule
{
    public function passes($attribute, $value)
    {
        return $value instanceof UploadedFile
            && ! preg_match('/\/Font/', file_get_contents($value->getPathname(), false, null, 0, 10_000));
    }

    public function message()
    {
        return 'PDF must not embed fonts.';
    }
}

Then drop it into the Form Request:

'attachment' => ['required', 'file', new SecureFile, new PdfHasNoEmbeddedFonts],

Cleaner for narrow / single-purpose checks than touching the package's validator.