diff --git a/src/Commands/DevCommand.php b/src/Commands/DevCommand.php index 260f4aad..e55ebca7 100644 --- a/src/Commands/DevCommand.php +++ b/src/Commands/DevCommand.php @@ -13,6 +13,7 @@ class DevCommand extends Command protected $signature = 'restify:dev {--path= : The path to the root local directory.} {--git : Use the latest vcs git repository} + {--revert : Revert composer.json to remove local development setup} '; protected $description = 'Add laravel-restify from a local directory.'; @@ -23,6 +24,10 @@ public function handle() return true; } + if ($this->option('revert')) { + return $this->revert(); + } + $this->addRepositoryToRootComposer(); $this->info('Added local path to repositories.'); @@ -127,4 +132,63 @@ private function resolveDefaultPath() ? 'git@github.com:BinarCode/laravel-restify.git' : '../../binarcode/laravel-restify'; } + + protected function revert(): int + { + $this->removeRepositoryFromComposer(); + + $this->info('Removed local path from repositories.'); + + $this->restorePackageVersion(); + + $this->info('Restored package version in composer.json.'); + + $this->composerUpdate(); + + $this->info('Composer updated. Development setup reverted.'); + + return 0; + } + + protected function removeRepositoryFromComposer(): void + { + $composer = json_decode(file_get_contents(base_path('composer.json')), true); + + if (! array_key_exists('repositories', $composer)) { + return; + } + + $composer['repositories'] = collect($composer['repositories'])->filter(function ($repository) { + if (! array_key_exists('url', $repository)) { + return true; + } + + return ! Str::contains($repository['url'], 'laravel-restify'); + })->values()->toArray(); + + if (empty($composer['repositories'])) { + unset($composer['repositories']); + } + + file_put_contents( + base_path('composer.json'), + json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + ); + } + + protected function restorePackageVersion(): void + { + $composer = json_decode(file_get_contents(base_path('composer.json')), true); + + if (! array_key_exists('require', $composer) || ! array_key_exists('binaryk/laravel-restify', $composer['require'])) { + return; + } + + $composer['require']['binaryk/laravel-restify'] = '^10.0'; + + file_put_contents( + base_path('composer.json'), + json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + ); + } } diff --git a/src/Fields/FieldCollection.php b/src/Fields/FieldCollection.php index db50df12..b301aa7b 100644 --- a/src/Fields/FieldCollection.php +++ b/src/Fields/FieldCollection.php @@ -198,4 +198,11 @@ public function inList(array $columns = []): self ->filter(fn (Field $field) => in_array($field->getAttribute(), $columns, true)) ->values(); } + + public function areFiles(): self + { + return $this + ->filter(fn (Field $field) => $field instanceof File) + ->values(); + } } diff --git a/src/Fields/File.php b/src/Fields/File.php index c235c354..7934d37e 100644 --- a/src/Fields/File.php +++ b/src/Fields/File.php @@ -12,7 +12,9 @@ use Carbon\CarbonInterface; use Closure; use Illuminate\Http\Request; +use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; +use RuntimeException; class File extends Field implements DeletableContract, StorableContract { @@ -81,8 +83,11 @@ public function storeAs($storeAs): self * * @return $this */ - public function resolveUsingTemporaryUrl(bool $resolveTemporaryUrl = true, ?CarbonInterface $expiration = null, array $options = []): self - { + public function resolveUsingTemporaryUrl( + bool $resolveTemporaryUrl = true, + ?CarbonInterface $expiration = null, + array $options = [] + ): self { if (! $resolveTemporaryUrl) { return $this; } @@ -161,13 +166,32 @@ public function storeSize($column) return $this; } + protected function resolveFileFromRequest(Request $request): ?UploadedFile + { + if (($file = $request->input($this->attribute)) instanceof UploadedFile && $file->isValid()) { + return $file; + } + + if (($file = $request->file($this->attribute)) && $file->isValid()) { + return $file; + } + + return null; + } + protected function storeFile(Request $request, string $requestAttribute) { + $file = $this->resolveFileFromRequest($request); + + if (! $file) { + throw new RuntimeException("No valid file found in the request for attribute {$requestAttribute}"); + } + if (! $this->storeAs) { - return $request->file($requestAttribute)->store($this->getStorageDir(), $this->getStorageDisk()); + return $file->store($this->getStorageDir(), $this->getStorageDisk()); } - return $request->file($requestAttribute)->storeAs( + return $file->storeAs( $this->getStorageDir(), is_callable($this->storeAs) ? call_user_func($this->storeAs, $request) : $this->storeAs, $this->getStorageDisk() @@ -196,7 +220,7 @@ public function store($storageCallback): self */ protected function mergeExtraStorageColumns($request, array $attributes): array { - $file = $request->file($this->attribute); + $file = $this->resolveFileFromRequest($request); if ($this->originalNameColumn) { $attributes[$this->originalNameColumn] = $file->getClientOriginalName(); @@ -229,6 +253,10 @@ protected function columnsThatShouldBeDeleted(): array public function fillAttribute(RestifyRequest $request, $model, ?int $bulkRow = null) { + if ($this->storeCallback instanceof Closure) { + return call_user_func($this->storeCallback, $request, $model, $this->attribute); + } + // Handle URL input first if ($request->has($this->attribute) && is_string($request->input($this->attribute))) { $url = $request->input($this->attribute); @@ -254,8 +282,7 @@ public function fillAttribute(RestifyRequest $request, $model, ?int $bulkRow = n } } - // Existing file upload logic - if (is_null($file = $request->file($this->attribute)) || ! $file->isValid()) { + if (! $this->resolveFileFromRequest($request)) { return $this; } diff --git a/src/MCP/Actions/SchemaAttributes.php b/src/MCP/Actions/SchemaAttributes.php index ab0de0c9..a21713b0 100644 --- a/src/MCP/Actions/SchemaAttributes.php +++ b/src/MCP/Actions/SchemaAttributes.php @@ -955,7 +955,7 @@ public function validateFile(string $attribute, $schema, array $parameters) return $existing; } - return $schema->string()->description('Must be a valid file'); + return $schema->string()->description('Must be a valid file, or file absolute path'); } /** @@ -1266,7 +1266,7 @@ public function validateMimes(string $attribute, $schema, array $parameters) return $schema->string()->description("Allowed file extensions: {$extensions}"); } - return $schema->string()->description('Must be a valid file with allowed extension'); + return $schema->string()->description('Must be a valid file, or file absolute path with allowed extension'); } /** @@ -1289,7 +1289,7 @@ public function validateMimetypes(string $attribute, $schema, array $parameters) return $schema->string()->description("Allowed MIME types: {$types}"); } - return $schema->string()->description('Must be a valid file with allowed MIME type'); + return $schema->string()->description('Must be a valid file, or file absolute path with allowed MIME type'); } /** diff --git a/src/MCP/Concerns/FieldMcpSchemaDetection.php b/src/MCP/Concerns/FieldMcpSchemaDetection.php index d0d1edee..f9986fc3 100644 --- a/src/MCP/Concerns/FieldMcpSchemaDetection.php +++ b/src/MCP/Concerns/FieldMcpSchemaDetection.php @@ -57,9 +57,15 @@ public function getDescription(RestifyRequest $request, Repository $repository): } } + $partialDescription = ''; + + if ($this instanceof File) { + $partialDescription = ' -- Important: This should be an absolute path or URL to the file that can be read.'; + } + if ($description = data_get($this->jsonSchema()?->toArray(), 'description')) { if (is_string($description)) { - return $description; + return $description.$partialDescription; } } @@ -78,11 +84,6 @@ public function getDescription(RestifyRequest $request, Repository $repository): } } - // Add file information for file fields - if ($this instanceof File) { - $description .= '. Upload a file'; - } - // Add examples based on field type and name if ($this->jsonSchema instanceof Type) { $examples = $this->generateFieldExamples($this->jsonSchema); @@ -92,7 +93,7 @@ public function getDescription(RestifyRequest $request, Repository $repository): } } - return $description; + return $description.' '.$partialDescription; } /** diff --git a/src/MCP/Concerns/McpStoreTool.php b/src/MCP/Concerns/McpStoreTool.php index d113e9c9..7aefba6f 100644 --- a/src/MCP/Concerns/McpStoreTool.php +++ b/src/MCP/Concerns/McpStoreTool.php @@ -3,7 +3,9 @@ namespace Binaryk\LaravelRestify\MCP\Concerns; use Binaryk\LaravelRestify\Fields\Field; +use Binaryk\LaravelRestify\Fields\File; use Binaryk\LaravelRestify\MCP\Requests\McpStoreRequest; +use Illuminate\Http\UploadedFile; use Illuminate\JsonSchema\JsonSchema; /** @@ -13,6 +15,40 @@ trait McpStoreTool { public function storeTool(McpStoreRequest $request): array { + $this->collectFields($request) + ->forStore($request, $this) + ->areFiles() + ->each(function (File $file) use ($request) { + if (! $request->has($file->attribute)) { + return; + } + + $filePath = $request->input($file->attribute); + $actualPath = null; + $fileName = null; + + if (file_exists($filePath) && is_readable($filePath)) { + $actualPath = $filePath; + $fileName = basename($filePath); + } elseif (filter_var($filePath, FILTER_VALIDATE_URL)) { + $actualPath = tempnam(sys_get_temp_dir(), 'upload_'); + file_put_contents($actualPath, file_get_contents($filePath)); + $fileName = basename(parse_url($filePath, PHP_URL_PATH)); + } + + if ($actualPath) { + $uploadedFile = new UploadedFile( + $actualPath, + $fileName, + mime_content_type($actualPath), + null, + true // Mark it as test mode to allow local files + ); + + $request->merge([$file->attribute => $uploadedFile]); + } + }); + return $this ->allowToStore($request) ->store($request)