Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
with:
php-version: ${{ matrix.php_version }}
coverage: none
extensions: mbstring, intl, pdo, pdo_sqlite, sqlite3
extensions: mbstring, intl, pdo, pdo_sqlite, sqlite3, fileinfo
ini-values: date.timezone=UTC

- name: symfony/flex is required to install the correct symfony version
Expand Down
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4|3.5.x-dev",
"league/flysystem-bundle": "^3.4",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
Expand All @@ -59,6 +60,9 @@
"symfony/process": "^5.4|^6.0|^7.0",
"symfony/web-link": "^5.4|^6.0|^7.0"
},
"suggest": {
"league/flysystem-bundle": "Allows to manage uploaded file destination"
},
"config": {
"sort-packages": true,
"allow-plugins": {
Expand Down
4 changes: 4 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use EasyCorp\Bundle\EasyAdminBundle\Adapter\UploadedFileAdapterFactory;
use EasyCorp\Bundle\EasyAdminBundle\ArgumentResolver\AdminContextResolver;
use EasyCorp\Bundle\EasyAdminBundle\ArgumentResolver\BatchActionDtoResolver;
use EasyCorp\Bundle\EasyAdminBundle\Asset\AssetPackage;
Expand Down Expand Up @@ -417,5 +418,8 @@

->set(Alert::class)
->tag('twig.component')

->set(UploadedFileAdapterFactory::class)
->arg(0, param('kernel.project_dir'))
;
};
23 changes: 21 additions & 2 deletions doc/fields/ImageField.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,26 @@ Basic Information
Options
-------

setBasePath
setUploadedFileAdapter
~~~~~~~~~~~~~~~~~~~~~~

You need to use this method to define how you want to store and read your files.
EasyAdmin provides two solutions: a classic local storage option using the ``LocalFileAdapter``,
or a solution compatible with S3 storage using the ``FlysystemFileAdapter``.
``FlysystemFileAdapter`` use the ``thephpleague/flysystem-bundle`` dependency to store the files.

To easily configure the content of the ``setUploadedFileAdapter`` method, we recommend using the ``UploadedFileAdapterFactory`` service, which offers two helper methods: ``createLocalFileAdapter`` and ``createFlysystemFileAdapter``::

yield ImageField::new('picture')
->setUploadedFileAdapter($this->uploadedFileAdapterFactory->createLocalFileAdapter('public/uploads/posts', 'uploads/posts'));

yield ImageField::new('picture')
->setUploadedFileAdapter($this->uploadedFileAdapterFactory->createFlysystemFileAdapter($this->defaultStorage));


Of course, you can also create your own storage system by implementing ``UploadedFileAdapterInterface``.

setBasePath (deprecated)
~~~~~~~~~~~

By default, images are loaded in read-only pages (``index`` and ``detail``) "as is",
Expand All @@ -36,7 +55,7 @@ without changing their path. If you serve your images under some path (e.g.

yield ImageField::new('...')->setBasePath('uploads/images/');

setUploadDir
setUploadDir (deprecated)
~~~~~~~~~~~~

By default, the contents of uploaded images are stored into files inside the
Expand Down
56 changes: 56 additions & 0 deletions src/Adapter/FlysystemFileAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Adapter;

use EasyCorp\Bundle\EasyAdminBundle\Decorator\FlysystemFile;
use League\Flysystem\FilesystemOperator;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
* @author Yoann Chocteau <[email protected]>
*/
class FlysystemFileAdapter implements UploadedFileAdapterInterface
{
public function __construct(private FilesystemOperator $fs)
{
}

public function supports(string $value): bool
{
return $this->fs->fileExists($value);
}

public function create(string $value): File
{
return new FlysystemFile($this->fs, $value);
}

public function publicUrl(string $value): string
{
if (method_exists($this->fs, 'publicUrl')) {
return $this->fs->publicUrl($value);
}
throw new \RuntimeException('Cannot generate public URL for this storage. Public URL generation was added in 3.6 of Flysystem. Please upgrade your Flysystem version.');
}

public function upload(UploadedFile $file, string $fileName): void
{
$stream = fopen($file->getPathname(), 'r');
if (false === $stream) {
throw new \RuntimeException('Failed to open file stream');
}
$this->fs->writeStream($fileName, $stream);
fclose($stream);
}

public function delete(File $file): void
{
$this->fs->delete($file->getPathname());
}

public function exists(string $value): bool
{
return $this->fs->fileExists($value);
}
}
66 changes: 66 additions & 0 deletions src/Adapter/LocalFileAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Adapter;

use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use function Symfony\Component\String\u;

/**
* @author Yoann Chocteau <[email protected]>
*/
class LocalFileAdapter implements UploadedFileAdapterInterface
{
public function __construct(private string $uploadDir, private string $basePath, private string $projectDir)
{
$relativeUploadDir = u($this->uploadDir)->trimStart(\DIRECTORY_SEPARATOR)->ensureEnd(\DIRECTORY_SEPARATOR)->toString();
$isStreamWrapper = filter_var($relativeUploadDir, \FILTER_VALIDATE_URL);
if ($isStreamWrapper) {
$absoluteUploadDir = $relativeUploadDir;
} else {
$absoluteUploadDir = u($relativeUploadDir)->ensureStart($this->projectDir.\DIRECTORY_SEPARATOR)->toString();
}

$this->uploadDir = $absoluteUploadDir;
}

public function supports(string $value): bool
{
return is_file($this->uploadDir.$value);
}

public function create(string $value): File
{
return new File($this->uploadDir.$value);
}

public function publicUrl(string $value): string
{
// add the base path only to images that are not absolute URLs (http or https) or protocol-relative URLs (//)
if (0 !== preg_match('/^(http[s]?|\/\/)/i', $value)) {
return $value;
}

// remove project path from filepath
$value = str_replace($this->projectDir.\DIRECTORY_SEPARATOR.'public'.\DIRECTORY_SEPARATOR, '', $value);

return \strlen($this->basePath) > 0
? rtrim($this->basePath, '/').'/'.ltrim($value, '/')
: '/'.ltrim($value, '/');
}

public function upload(UploadedFile $file, string $fileName): void
{
$file->move($this->uploadDir, $fileName);
}

public function delete(File $file): void
{
unlink($file->getPathname());
}

public function exists(string $value): bool
{
return file_exists($this->uploadDir.$value);
}
}
28 changes: 28 additions & 0 deletions src/Adapter/UploadedFileAdapterFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Adapter;

use League\Flysystem\FilesystemOperator;

/**
* @author Yoann Chocteau <[email protected]>
*/
class UploadedFileAdapterFactory
{
private string $projectDir;

public function __construct(string $projectDir)
{
$this->projectDir = $projectDir;
}

public function createLocalFileAdapter(string $uploadDir, string $basePath): LocalFileAdapter
{
return new LocalFileAdapter($uploadDir, $basePath, $this->projectDir);
}

public function createFlysystemFileAdapter(FilesystemOperator $filesystemOperator): FlysystemFileAdapter
{
return new FlysystemFileAdapter($filesystemOperator);
}
}
24 changes: 24 additions & 0 deletions src/Adapter/UploadedFileAdapterInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Adapter;

use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
* @author Yoann Chocteau <[email protected]>
*/
interface UploadedFileAdapterInterface
{
public function supports(string $value): bool;

public function create(string $value): File;

public function publicUrl(string $value): string;

public function upload(UploadedFile $file, string $fileName): void;

public function delete(File $file): void;

public function exists(string $value): bool;
}
4 changes: 2 additions & 2 deletions src/Attribute/AdminDashboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ public function __construct(
/**
* @var string|null $routePath The path of the Symfony route that will be created for the dashboard (e.g. '/admin)
*/
public /* ?string */ $routePath = null,
/* ?string */ public $routePath = null,
/**
* @var string|null $routeName The name of the Symfony route that will be created for the dashboard (e.g. 'admin')
*/
public /* ?string */ $routeName = null,
/* ?string */ public $routeName = null,
/**
* @var array{
* requirements?: array<string, string>,
Expand Down
8 changes: 4 additions & 4 deletions src/Controller/AbstractCrudController.php
Original file line number Diff line number Diff line change
Expand Up @@ -626,22 +626,22 @@ protected function processUploadedFiles(FormInterface $form): void
continue;
}

$uploadedFileAdapter = $config->getOption('uploaded_file_adapter');
$uploadDelete = $config->getOption('upload_delete');

if ($state->hasCurrentFiles() && ($state->isDelete() || (!$state->isAddAllowed() && $state->hasUploadedFiles()))) {
foreach ($state->getCurrentFiles() as $file) {
$uploadDelete($file);
$uploadDelete($file, $uploadedFileAdapter);
}
$state->setCurrentFiles([]);
}

$filePaths = (array) $child->getData();
$uploadDir = $config->getOption('upload_dir');
$uploadNew = $config->getOption('upload_new');

foreach ($state->getUploadedFiles() as $index => $file) {
$fileName = u($filePaths[$index])->replace($uploadDir, '')->toString();
$uploadNew($file, $uploadDir, $fileName);
$fileName = u($filePaths[$index]);
$uploadNew($file, $fileName, $uploadedFileAdapter);
}
}
}
Expand Down
28 changes: 28 additions & 0 deletions src/Decorator/FlysystemFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Decorator;

use League\Flysystem\FilesystemOperator;
use Symfony\Component\HttpFoundation\File\File;

class FlysystemFile extends File
{
private FilesystemOperator $filesystemOperator;

public function __construct(FilesystemOperator $filesystemOperator, string $path)
{
$this->filesystemOperator = $filesystemOperator;

parent::__construct($path, false);
}

public function getSize(): int
{
return $this->filesystemOperator->fileSize($this->getPathname());
}

public function getMTime(): int
{
return $this->filesystemOperator->lastModified($this->getPathname());
}
}
Loading
Loading