Blog

Managing files in MonkeysLegion with monkeyslegion-files

Posted by adminJuly 27, 2025

Uploads, storage drivers (local/S3/GCS), handy helper functions, and a small controller example you can copy-paste.

What you get

  • UploadManager – validates and stores multipart/form-data uploads.
  • Storage drivers – local built-in; optional S3 / Google Cloud Storage.
  • Deterministic paths – date-based folders plus SHA-256 slug.
  • Helpers – simple globals for quick writes/reads/signing URLs.
  • .mlc config – one place to switch disks and limits.

Repo: monkeyscloud/monkeyslegion-files.

1) Install

composer require monkeyscloud/monkeyslegion-files
# optional drivers
composer require aws/aws-sdk-php           # for S3
composer require google/cloud-storage      # for GCS

The package autoloads helpers and registers a service provider via Composer.

2) Add config/files.mlc

Create config/files.mlc (note: MLC uses curly braces, not TOML sections):

files {
  default_disk = "local"
  max_bytes    = 20971520
  mime_allow   = ["image/jpeg","image/png","image/webp","application/pdf"]

  disks {
    local {
      root            = "storage/app"
      public_base_url = "files"
    }

    # s3 {
    #   bucket           = "my-s3-bucket"
    #   region           = "us-east-1"
    #   prefix           = "uploads/"
    #   public_base_url  = "https://my-cloudfront/"
    # }

    # gcs {
    #   project_id       = "my-gcp-project"
    #   key_file_path    = "/path/to/key.json"
    #   bucket           = "my-gcs-bucket"
    #   prefix           = "uploads/"
    #   public_base_url  = "https://cdn.example.com"
    # }
  }
}

What these mean

  • default_disk – which driver to bind: local, s3, or gcs.

  • max_bytes – per-file cap; uploads larger than this are rejected.

  • mime_allow – allowed MIME types (UploadManager validates).

  • root – local filesystem root. The provider will mkdir -p this if missing.

  • public_base_url

    • For local, set a path fragment like "files" (or "/files"). The provider will create public/files so you can serve it directly.

    • For CDN/full URLs, set e.g. "https://cdn.example.com". The driver will return absolute URLs.

3) Ensure the provider is registered

monkeyslegion-files ships a service provider. It must be invoked on the ContainerBuilder (not the built container). Add one line in your bootstrap:

// MonkeysLegion\Framework\HttpBootstrap::buildContainer()

use MonkeysLegion\Files\Support\ServiceProvider as FilesServiceProvider;

$b = new ContainerBuilder();
$b->addDefinitions((new AppConfig())());
if (is_file($root.'/config/app.php')) {
    $b->addDefinitions(require $root.'/config/app.php');
}

/* Register the files provider on the BUILDER */
(new FilesServiceProvider())->register($b);

$container = $b->build();

The provider binds:

  • MonkeysLegion\Files\Contracts\FileStorage → Local/S3/GCS driver

  • MonkeysLegion\Files\Upload\UploadManager

  • MonkeysLegion\Files\Contracts\FileNamerHashPathNamer

It also creates storage/app and public/{public_base_url} when needed.

4) Create an entity (optional)

Example Media entity:

#[Entity]
class Media
{
    #[Field(type: 'INT', autoIncrement: true, primaryKey: true)]
    public int $id;

    #[Field(type: 'string')]
    public string $url;

    #[Field(type: 'string', nullable: true)]
    public ?string $type = null;

    // getters / setters …
}

5) Controller: handle uploads

<?php

declare(strict_types=1);

namespace App\Controller;

use App\Entity\Media;
use MonkeysLegion\Files\Upload\UploadManager;
use MonkeysLegion\Repository\RepositoryFactory;
use MonkeysLegion\Router\Attributes\Route;
use MonkeysLegion\Http\Message\JsonResponse;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;

final class MediaController
{
    public function __construct(
        private UploadManager     $uploads,
        private RepositoryFactory $repos
    ) {}

    #[Route(methods: 'POST', path: '/media/upload')]
    public function upload(ServerRequestInterface $request): JsonResponse
    {
        try {
            // expects field name "file"
            $meta = $this->uploads->handle($request, 'file');
        } catch (\Throwable $e) {
            throw new RuntimeException('Upload failed: '.$e->getMessage(), 400);
        }

        $media = new Media();
        $media->setUrl($meta->url ?? $meta->path);   // prefer public URL, fallback to path
        $media->setType($meta->mimeType);

        $repo = $this->repos->getRepository(Media::class);
        $repo->save($media);

        return new JsonResponse([
            'id'   => $media->getId(),
            'url'  => $media->getUrl(),
            'type' => $media->getType(),
        ], 201);
    }
}

Test with curl

curl -i -X POST http://127.0.0.1:8000/media/upload \
  -H "Accept: application/json" \
  -F "file=@/absolute/path/to/image.jpg;type=image/jpeg"

On success:

HTTP/1.1 201 Created
{
  "id": 1,
  "url": "files/2025/07/26/abcd1234ef567890.jpg",
  "type": "image/jpeg"
}

If url is null, your disk is private; use ml_files_url() or a signed URL (below), or switch to a driver/CDN that returns public URLs.

6) Helper functions you can use anywhere

The package autoloads src/helpers.php:

// Store a PSR-7 stream and get rich metadata
$meta = ml_files_put($psr7Stream, 'photo.jpg', 'image/jpeg');

// Store a raw string quickly; returns the storage path
$path = ml_files_put_string('hello', 'text/plain', 'hello.txt');

// Store a local file; returns the storage path
$path = ml_files_put_path('/tmp/report.pdf');

// Read back as PSR-7 stream
$stream = ml_files_read_stream($path);

// Check/Remove
$exists = ml_files_exists($path);
ml_files_delete($path);

// Build a public URL (driver method or config fallback)
$url = ml_files_url($path);

// Signed URL (HMAC SHA-256)
$signed = ml_files_sign_url('/files/'.$path, 600);   // 10 minutes
$isValid = ml_files_verify_signature($signed);

7) Switching to S3 or GCS

S3

  1. Install the SDK:

composer require aws/aws-sdk-php
  1. Update files.mlc:

files {
  default_disk = "s3"
  # …
  disks {
    s3 {
      bucket           = "my-s3-bucket"
      region           = "us-east-1"
      prefix           = "uploads/"
      public_base_url  = "https://cdn.example.com"   # optional
    }
  }
}
  1. Provide credentials via environment (standard AWS mechanism), or however your infra does it (instance role, etc.).

Google Cloud Storage

  1. Install:

composer require google/cloud-storage
  1. Config:

files {
  default_disk = "gcs"
  disks {
    gcs {
      project_id      = "my-gcp-project"
      key_file_path   = "/absolute/path/to/key.json"  # or omit to use ADC
      bucket          = "my-gcs-bucket"
      prefix          = "uploads/"
      public_base_url = "https://cdn.example.com"     # optional
    }
  }
}

8) Next.js example (frontend)

const onUpload = async (file: File) => {
  const fd = new FormData();
  fd.append('file', file);

  const res = await fetch('/media/upload', {
    method: 'POST',
    body: fd,
  });

  if (!res.ok) throw new Error(await res.text());
  const data = await res.json(); // { id, url, type }
  return data;
};

9) Troubleshooting

Disk 'local' not configured in files.mlc

Ensure your file is wrapped like files { … } and that files.disks.local exists. The provider reads files.default_disk and files.disks.

“Syntax error … at: [disks.local]”

MLC is not TOML. Use curly braces:

disks { local { … } }, not [disks.local].

Cannot resolve constructor parameter $storage

The provider didn’t run. Make sure you call:

(new \MonkeysLegion\Files\Support\ServiceProvider())->register($builder);

before $builder->build().

Call to undefined method Container::set()

Register on the ContainerBuilder, not on the built Container.

helpers.php not found

Confirm the package has autoload.files: ["src/helpers.php"]. If your vendor copy placed it at the root, either move it under src/ or change the autoload path, then composer dump-autoload -o.

Invalid upload for field 'file'

Some stacks provide a raw $_FILES array. The current UploadManager normalizes that to a PSR-7 UploadedFileInterface. Verify your client uses field name file and sends multipart/form-data.

Permissions

Make sure storage/app and public/files are writable by the PHP process.

10) Summary

  • Put your disk settings in config/files.mlc under a files { … } root.

  • Register the Files ServiceProvider on the builder.

  • Use UploadManager in your controllers, or call the global helpers.

  • Switch to S3/GCS by changing default_disk and filling the disk block.

  • For public serving on local, point your web server to public/files/ (or whatever you configured in public_base_url), or use the signing helper for private access.

That’s it—happy uploading! If you run into anything else, share the stack trace plus your files.mlc, and you’ll spot the fix fast.