1
votes

I got a class use to upload file as service like symfony documentation. https://symfony.com/doc/current/controller/upload_file.html#creating-an-uploader-service

I use symfony 5.

When i declare service in main config/services.yaml, this work.

But i have a bundle for file management and i want to put service declaration in this bundle : App/AD/ExplorerBundle/Resources/config/services.yaml.

When i do that this doesn't work anymore.

I have error

Cannot resolve argument $fileUploader of "App\AD\ExplorerBundle\Controller\FileController::addAction()": Cannot autowire service "App\AD\ExplorerBundle\Service\FileUploader": argument "$targetDirectory" of method "__construct()" is type-hinted "string", you should configure its value explicitly.

I don't understand why, because _defaults autoconfigure and autowire = true

I test cache:clear, reload server, but nothing work.

Any help will be apreciate

Edit : my bundle extension: in AD\ExplorerBundle\DependencyInjection

<?php

namespace App\AD\ExplorerBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

/**
 * This is the class that loads and manages your bundle configuration.
 *
 * @link http://symfony.com/doc/current/cookbook/bundles/extension.html
 */
class ADExplorerExtension extends Extension
{
    /**
     * {@inheritdoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yml');
    }
}

my bundle service : in AD\ExplorerBundle\Service

<?php
namespace App\AD\ExplorerBundle\Service;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\String\Slugger\SluggerInterface;

class FileUploader
{
    private $targetDirectory;
    private $slugger;

    public function __construct(string $targetDirectory, SluggerInterface $slugger)
    {
        $this->targetDirectory = $targetDirectory;
        $this->slugger = $slugger;
    }

    public function upload(UploadedFile $file): array
    {
        $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
        $safeFilename = $this->slugger->slug($originalFilename);
        $fileName = $safeFilename.'-'.uniqid().'.'.$file->guessExtension();

        $result = array(
            'filename' => $fileName,
            'originalName' => $originalFilename,
            'extension' => $file->guessExtension()
                );

        try {
            $file->move($this->getTargetDirectory(), $fileName);
        } catch (FileException $e) {
            // ... handle exception if something happens during file upload
        }

        return $result;
    }

    public function getTargetDirectory()
    {
        return $this->targetDirectory;
    }
}

my config/services.yaml

parameters:
    locale: 'fr'
    doctrine_behaviors_translatable_fetch_mode: "LAZY"
    doctrine_behaviors_translation_fetch_mode: "LAZY"



imports:
    - { resource: '@ADCoreBundle/Resources/config/services.yml' }
    - { resource: './parameters.yaml' }

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    App\:
        resource: '../src/*'
        exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'


    App\Controller\:
        resource: '../src/Controller'
        tags: ['controller.service_arguments']

my Bundle service : in AD\ExplorerBundle\Resources\config\service.yaml

parameters:
    brochures_directory: '%kernel.project_dir%/public/uploads'

services: 
    ad_file_uploader:         
        class: App\AD\ExplorerBundle\Service\FileUploader
        arguments:
            $targetDirectory: '%brochures_directory%'

I read documentation : https://symfony.com/doc/current/bundles/extension.html

https://symfony.com/doc/current/service_container.html#manually-wiring-arguments

https://symfony.com/doc/current/service_container/autowiring.html

I don't really understand this :

Public and Reusable Bundles¶

Public bundles should explicitly configure their services and not rely on autowiring. Autowiring depends on the services that are available in the container and bundles have no control over the service container of applications they are included in. You can use autowiring when building reusable bundles within your company, as you have full control over all code.

I think it's ok because it's my bundle and my application so i have full control of code.

So, i test a lot of thing but nothing work.

If anybody have an idea Thanks

1
Is the bundle source code in the same src directory as your application?Cerad
yes the bundle is in src/AD/ExplorerBundle/ folder of my application, the service : src/AD/ExplorerBundle/Service/ and the yaml in src/AD/ExplorerBundle/Resource/config/threeside
Thought so. First off, out of the box, autowire scans every php file under src and attempts to create a service. What is happening is that the application is trying to autowire your service and cannot deal with the $targetDir string. Moving the source code for your bundle to a different directory, say src2, and adjusting the psr4 autoload will remove this particular error. Secondly, bundles should not be autowired. Adjust your bundle's service.yaml file and manually wire up the service. Finally, only make a bundle if you plan to reuse it in multiple apps.Cerad
Thanks for help Cerad. I'm not sure to understand. Why my service work when i declare it in main config/services.yaml ? You said Bundle should not be autowired, you suppose i remove App\: resource: '../src/*' exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' in my services.yaml ?threeside
If you are determined to use bundles then follow the guidance in the documentation. If you wish to do things your own way then spend the time to understand the implementation details of autowire. I can't offer any shortcuts.Cerad

1 Answers

2
votes

Symfony recommends using manual service definitions in bundles mostly to avoid the overhead of constantly scanning the bundle everytime the cache is rebuilt. But I thought it might be interesting to see what creating a potentially reusable bundle with autowire/autoconfigure actually entails. Tried to follow the bundles best practices as much as I could.

For my own reference, I checked in a complete working bundle example.

Ultimately bundles should end up in their own repository. However, it can be easier to develop a bundle by enclosing it inside an application. This is the approach I used. But it is important not to try and mix your app source code with the bundle source code. Doing so is not only challenging but will make it difficult to copy your bundle into it's own repository.

So we start with a directory structure like this:

project # Symfony application project
    src: # Application source code
    explorer-bundle # AD Explorer Bundle source code
        ADExplorerBundle.php

Getting your namespace right is also important. The prefix really should be a unique vendor itdentifier just to avoid possible naming conflicts. In this case, we use AD. And then of course, since AD might have multiple bundles, we further sub-divide with a bundle specific identifier.

namespace AD\ExplorerBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class ADExplorerBundle extends Bundle
{

}

In order for autoloading to work we tweak the composer.json file. Once the bundle is converted into a true composer package, this line will no longer be needed. And of course you have to add the bundle class to project/config/bundles.php

# composer.json
    "autoload": {
        "psr-4": {
            "App\\": "src/",
            "AD\\ExplorerBundle\\": "explorer-bundle/"
        }
    },
# run composer "dump-autoload" after making this change

So now we need an extension to load the bundle's services.yaml. It's just a standard load so no need to show the code here. This is what the services.yaml file looks like:

# explorer-bundle/Resources/config/services.yaml
parameters:
    # Cheating here, this would normally be part of the bundle configuration
    ad_explorer_target_directory: 'some_directory'

services:
    _defaults:
        autowire: true
        autoconfigure: true
        bind:
            $targetDirectory: '%ad_explorer_target_directory%'

    AD\ExplorerBundle\:
        resource: '../../*'
        exclude: '../../{DependencyInjection,Entity,Migrations,Tests,ADExplorerBundle.php}'

    AD\ExplorerBundle\Controller\:
        resource: '../../Controller'
        tags: ['controller.service_arguments']

To keep things simple, I just made the target directory a parameter. You would probably want to make an actual bundle configuration and give the application the ability to set it. But that is outside the scope of this answer.

For testing I chose to create a command. I find it easier than trying to refresh the browser.

// Easier to debug an injected service using a command than a controller
class ExplorerAddCommand extends Command
{
    private $fileUploader;

    protected static $defaultName = 'ad:explorer:add';

    public function __construct(FileUploader $fileUploader)
    {
        parent::__construct();
        $this->fileUploader = $fileUploader;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        echo "Explorer Add " . $this->fileUploader->targetDirectory . "\n";
        return 0;
    }
}

And that is it.