I researched the How to Handle File Uploads with Doctrine and I don't want to hard-code the __DIR__.'/../../../../web/'.$this->getUploadDir(); path because maybe in future I will change the web/ directory. How to do it more flexible? I found this but it doesn't answer the question how to do it more flexible from inside the Entity
You shouldn't use entity class as a form model here. It's simply not suitable for that job. If the entity has the path property, the only valid values it can stores are: null (in case lack of the file) and string representing the path to the file.
Create a separate class, that's gonna be a model for your form:
class MyFormModel {
/** #Assert\File */
private $file;
/** #Assert\Valid */
private $entity;
// constructor, getters, setters, other methods
}
In your form handler (separate object configured through DIC; recommended) or the controller:
...
if ($form->isValid()) {
/** #var \Symfony\Component\HttpFoundation\File\UploadedFile */
$file = $form->getData()->getFile();
/** #var \Your\Entity\Class */
$entity = $form->getData()->getEntity();
// move the file
// $path = '/path/to/the/moved/file';
$entity->setPath($path);
$someEntityManager->persist($entity);
return ...;
}
...
Inside form handler/controller you can access any dependencies/properties from DIC (including path to the upload directory).
The tutorial you've linked works, but it's an example of bad design. The entities should not be aware of file upload.
To access the root directory from outside the controller you can simply inject '%kernel.root_dir%' as an argument in your services configuration.
service_name:
class: Namespace\Bundle\etc
arguments: ['%kernel.root_dir%']
Then you can get the web root in the class constructor:
public function __construct($rootDir)
{
$this->webRoot = realpath($rootDir . '/../web');
}
You can use a variable in your parameters.yml.
Like this you'll can change path when you want.
for example :
# app/config/parameters.yml
# Upload directories
upload_avatar_dir: /uploads/avatars
upload_content_dir: /uploads/content
upload_product_offer_dir: /uploads/product-offer
...
I handled this by creating an abstract class that Entities may extend if they are handling file uploads as described in the Symfony Documentation. I created the files array so I could create a copy of the existing file path in the set methods so it could be deleted off the file system on a successful update or delete without defining any additional properties in the Entity proper.
use Symfony\Component\HttpFoundation\File\File;
abstract class FileUploadEntity
{
private $files;
public function __set($name, File $value)
{
$this->files[$name] = $value;
}
public function __get($name)
{
if (!is_array($this->files)) $this->files = array();
if (!array_key_exists($name, $this->files)) {
return null;
}
return $this->files[$name];
}
public function getUploadRootDirectory()
{
return $this->getWebDirectory() . $this->getUploadDirectory();
}
public function getWebDirectory()
{
return __DIR__ . "/../../../../web/";
}
public function getUploadDirectory()
{
$year = date("Y");
$month= date("m");
return "images/uploads/$year/$month/";
}
public function getEncodedFilename($name)
{
return sha1($name . uniqid(mt_rand(), true));
}
// this should be a PrePersist method
abstract public function processImages();
// This should be defined as a Doctrine PreUpdate Method
abstract public function checkImages();
// this should be a PostPersist method
abstract public function upload();
// this should be a PostUpdate method and delete old files
abstract public function checkUpload();
// This should be a PostRemove method and delete files
abstract public function deleteFile();
}
Related
first of all I am trying to use services for the first time... (Actually if s.o. could give a short info about how and when and why to use it.. nice ;-) )
But now to my specific case:
I wrote two controllers:
One for uploading a xlsx file to the server
One for importing the xlsx data to the DB
What I now want to do is to pass the (uploaded)path from the uploading controller to the import controller. am I correct to use the import as a service?
Code looks as the following...
class FileUploadController extends Controller
/**
* #Route("/upload", name="upload")
* #Security("has_role('ROLE_ADMIN')")
*/
public function uploadAction(Request $request){
$companyid = $this->getUser()->getCompany();
if ($request->getMethod() == 'POST'){
$file = $request->files->get('xls');
$uploadedURL = '';
if(($file instanceof UploadedFile) && $file->getError()=='0'){
if(!($file->getSize()<20000)){
$originalName = $file->getClientOriginalName();
$name_array = explode('.',$originalName );
$file_type = $name_array[(sizeof($name_array)-1)];
$valid_filetypes = array('xls', 'xlsx');
if(in_array(strtolower($file_type), $valid_filetypes)){
$document = new Document();
$document->setFile($file);
$document->setSubDirectory('uploads');
$document->processFile();
$uploadedURL=$uploadedURL=$document->getUploadDirectory().DIRECTORY_SEPARATOR.$document->getSubDirectory().DIRECTORY_SEPARATOR.$file->getBasename();
}else{
echo "Wrong File Ending";
}
}else {
echo "File to big";
}
}else{
print_r('File Error');
die;;
}
$this->get("dataimport.service")->importIndexAction($uploadedURL);
}else{
return $this->render(bla)
DataImportController as:
class DataImportController extends Controller
/**
* #param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* #Security("has_role('ROLE_ADMIN')")
*/
public function importIndexAction($path)
{
$companyid = $this->getUser()->getCompany();
$em = $this->getDoctrine()->getManager();
$file = $this->defineFilePathAction($path);
$reader = $this->readExcelAction($file);
$accountarray = $this->getAccountsArrayAction($companyid);
$this->importAccountsAction($companyid, $reader, $accountarray, $em);
}
....
/**
* Get a service from the container
*
* #param string The service to get
*/
public function get($service)
{
return $this->container->get($service);
}
services.yml
services:
dataimport.service:
class: AppBundle\Controller\DataHandling\DataImportController
arguments: [#service_container]
Thanks for your help!
In Symfony a controller and service is at first just a class. A controller's public method is meant to take an input and generates an Response (output) (by the way injecting the Request is deprecated, you have to use the current request from the request_stack). A service is an object out of the DI container with no constraints at all.
Since the controller's method has to generate and return a response, it's mostly not a good idea to invoke a controller from another controller, because you maybe don't need that response, but only the implementation of the method.
That's also the reason why you should move reusable code to services. A controller should actually only:
extract data from the request
call some services
render a template with the result
Same for Commands. The services are the core of your application. Horizontal communication between controllers or commands is mostly a bad idea (only of course some proxies or wrapper).
Here are some ideas for your code:
The action itself is too much unreadable code. If you get the uploaded file via symfony form read this https://stackoverflow.com/a/28754907/4469738
Don't access the request directly if you use forms. The reason is, that only your builder or Type class which creates the form (and the data class), knows the name of the input fields and maps them to a data class. You should just use the data class. Then you get a nice UploadedFile object to check everything, but also move the checks to services.
What is the best way to access configuration values inside an entity in a symfony 2 application?
I've searched about this and i've found two solutions:
Define the entity as a service and inject the service container to access configuration values
And this approach which defines a class in the same bundle of the entity with static methods that allows to get the parameter value
Is there any other solution? What's the best workaround?
Your entity shouldn't really access anything else, apart from associated entities. It shouldn't really have any connection outwardly to the outside world.
One way of doing what you want would be to use a subscriber or listener to listen to the entity load event and then pass that value in to the entity using the usual setter.
For example....
Your Entity
namespace Your\Bundle\Entity;
class YourClass
{
private $parameter;
public function setParameter($parameter)
{
$this->parameter = $parameter;
return $this;
}
public function getParameter()
{
return $this->parameter;
}
...
}
Your Listener
namespace Your\Bundle\EventListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Your\Bundle\Entity\YourEntity;
class SetParameterSubscriber implements EventSubscriber
{
protected $parameter;
public function __construct($parameter)
{
$this->parameter = $parameter;
}
public function getSubscribedEvents()
{
return array(
'postLoad',
);
}
public function postLoad(LifecycleEventArgs $args)
{
/** #var YourEntity $entity */
$entity = $args->getEntity();
// so it only does it to your YourEntity entity
if ($entity instanceof YourEntity) {
$entity->setParameter($this->parameter);
}
}
}
Your services file.
parameters:
your_bundle.subscriber.set_parameter.class:
Your\Bundle\EventListener\SetParameterSubscriber
// Should all be on one line but split for readability
services:
your_bundle.subscriber.set_parameter:
class: %your_bundle.subscriber.set_parameter.class%
arguments:
- %THE PARAMETER YOU WANT TO SET%
tags:
- { name: doctrine.event_subscriber }
You shouldn't need a configuration in your entity.
For example you have File entity and you need to save a file represented by this entity to a disk. You need some parameter, let say "upload_dir". You can pass somehow this parameter to the entity and define a method inside this entity which saves a file to upload dir. But better way would be create a service which would be responsible for saving files. Then you can inject configurtion into it and in save method pass entity object as an argument.
File structure
customerService.PHP
include 'vo/VOCustomer.php';
include 'mydb.php';
class customerService
{
public function createCustomer(VOCustomer $cus)
{
$db = new mydb();
$db->connect();
$query = sprintf("insert into customer (CusId, CusName, CusContact,idcompany) values ('%s','%s','%s','%s')",
mysql_real_escape_string($cus->CusId),
mysql_real_escape_string($cus->CusName),
mysql_real_escape_string($cus->CusContact),
mysql_real_escape_string($cus->idcompany));
$rs = mysql_query($query) or die ("Unable to complete query.");
return 'success';
}
}
vo/VOCustomer.php
class VOCustomer {
public $CusId;
public $CusName;
public $CusContact;
public $idcompany;
}
When importing the customerService.php to a flex zend project Its possible that the data type may not return as VOCustomer sometimes it will show Object as type
How to make the passing object as VOcustomer object ?
I'm not sure the 'Connect to PHP' wizard understands type hinting.
Even if it does Zend AMF will pass an Objet not a VOCustomer to the method.
It's safer to add a PHPDoc comment:
/**
* #param VOCustomer $cus
*/
public function createCustomer($cus)
Second add dummy function to your service that returns VOCustomer. The 'Connect to PHP' wizard generates a value object only if it's returned by a service method.
/**
* #return VOCustomer
*/
public function getCustomer() {
//Do nothing
}
I want to get an array from a yaml file inside one of my services, and I am a little confused of how to inject the file to use in my services.yml.
# /path/to/app/src/Bundle/Resources/config/services.yml
parameters:
do_something: Bundle\DoSomething
yaml.parser.class: Symfony\Component\Yaml\Parser
yaml.config_file: "/Resources/config/config.yml" # what do I put here to win!
services:
yaml_parser:
class: %yaml.parser.class%
do_parsing:
class: %do_something%
arguments: [ #yaml_parser, %yaml.config_file% ]
In my service I have
# /path/to/app/src/Bundle/DoSomething.php
<?php
namespace Bundle;
use \Symfony\Component\Yaml\Parser;
class DoSemething
{
protected $parser;
protected $parsed_yaml_file;
public function __construct(Parser $parser, $file_path)
{
$this->parsed_yaml_file = $parser->parse(file_get_contents(__DIR__ . $file_path));
}
public function useParsedFile()
{
foreach($parsed_yaml_file as $k => $v)
{
// ... etc etc
}
}
}
This may be the completely wrong approach, if I should be doing something else please let me know!
First I'll explain why I implemented my solution for you to decide if this case is right for you.
I needed a way to easily load custom .yml files in my bundle (for lots of bundles) so adding a separate line to app/config.yml for every file seemed like a lot of hassle for every setup.
Also I wanted most of the configs to be already loaded by default so end-user wouldn't even need to worry about configuring most of the time, especially not checking that every config file is setup correctly.
If this seems like a similar case for you, read on. If not, just use Kris solution, is a good one too!
Back when I encountered a need for this feature, Symfony2 didnt't provide a simple way to achieve this, so here how I solved it:
First I created a local YamlFileLoader class which was basically a dumbed down Symfony2 one:
<?php
namespace Acme\DemoBundle\Loader;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Config\Loader\FileLoader;
/**
* YamlFileLoader loads Yaml routing files.
*/
class YamlFileLoader extends FileLoader
{
/**
* Loads a Yaml file.
*
* #param string $file A Yaml file path
*
* #return array
*
* #throws \InvalidArgumentException When config can't be parsed
*/
public function load($file, $type = null)
{
$path = $this->locator->locate($file);
$config = Yaml::parse($path);
// empty file
if (null === $config) {
$config = array();
}
// not an array
if (!is_array($config)) {
throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $file));
}
return $config;
}
/**
* Returns true if this class supports the given resource.
*
* #param mixed $resource A resource
* #param string $type The resource type
*
* #return Boolean True if this class supports the given resource, false otherwise
*
* #api
*/
public function supports($resource, $type = null)
{
return is_string($resource) && 'yml' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'yaml' === $type);
}
}
Then I updated DIC Extension for my bundle (it's usually generated automatically if you let Symfony2 create full bundle architecture, if not just create a DependencyInjection/<Vendor&BundleName>Extension.php file in your bundle directory with following content:
<?php
namespace Acme\DemoBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
use Acme\DemoBundle\Loader\YamlFileLoader;
/**
* This is the class that loads and manages your bundle configuration
*
* To learn more see {#link http://symfony.com/doc/current/cookbook/bundles/extension.html}
*/
class AcmeDemoExtension extends Extension
{
/**
* {#inheritDoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.xml');
// until here everything is default config (for your DIC services)
$ymlLoader = new YamlFileLoader(new FileLocator(__DIR__.'/../Resources/config'));
$container->setParameter('param_name', $ymlLoader->load('yaml_file_name'))); // load yml file contents as an array
}
}
And now you can access/pass your yaml config as simple service parameter (i.e. %param_name% for services.yml)
I solved it this way:
Services.yml
#/path/to/app/src/Bundle/Resources/config/services.yml
parameters:
example.class: Path\To\Bundle\Service\Class
example.yaml_config_file: "%kernel.root_dir%/../src/Path/To/Bundle/Resources/config/config.yml"
services:
example_service:
class: %example.class%
arguments: [%example.yaml_config_file% ]
Service class
# /path/to/app/src/Bundle/Service/Example.php
<?php
namespace Bundle\Service;
use \Symfony\Component\Yaml\Yaml;
class Example
{
private $parsed_yaml_file;
public function __construct($yaml_config_file)
{
$this->parsed_yaml_file = Yaml::parse($yaml_config_file);
}
}
You can use the kernel.root_dir parameter:
parameters:
yaml.config_file: "%kernel.root_dir%/../src/Path/To/MyBundle/Resources/config/config.yml"
If you're using Symfony 3.3 or higher, you can now also use the new kernel.project_dir parameter.
This parameter points to the highest level directory containing a composer file.
I'm using Symfony 2 with Doctrine 2.
I need to encrypt a field in my entity using an encryption service, and I'm wondering where should I put this logic.
I'm using a Controller > Service > Repository architecture.
I was wondering if a listener would be a good idea, my main concern is, if my entity is stored encrypted, if I decrypt it on the fly its state it's gonna be changed and I'm not sure it's a good idea.
How would you implement this?
To expand on richsage and targnation's great answers, one way to inject a dependency (e.g., cypto service) into a custom Doctrine mapping type, could be to use a static property and setter:
// MyBundle/Util/Crypto/Types/EncryptedString.php
class EncryptedString extends StringType
{
/** #var \MyBundle\Util\Crypto */
protected static $crypto;
public static function setCrypto(Crypto $crypto)
{
static::$crypto = $crypto;
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
$value = parent::convertToDatabaseValue($value, $platform);
return static::$crypto->encrypt($value);
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
$value = parent::convertToPHPValue($value, $platform);
return static::$crypto->decrypt($value);
}
public function getName()
{
return 'encrypted_string';
}
}
Configuration would look like this:
// MyBundle/MyBundle.php
class MyBundle extends Bundle
{
public function boot()
{
/** #var \MyBundle\Util\Crypto $crypto */
$crypto = $this->container->get('mybundle.util.crypto');
EncryptedString::setCrypto($crypto);
}
}
# app/Resources/config.yml
doctrine:
dbal:
types:
encrypted_string: MyBundle\Util\Crypto\Types\EncryptedString
# MyBundle/Resources/config/services.yml
services:
mybundle.util.crypto:
class: MyBundle\Util\Crypto
arguments: [ %key% ]
I don't know if it's the right way at all, but I implemented this recently by creating a custom mapping type, as per the Doctrine docs. Something like the following:
class EncryptedStringType extends TextType
{
const MYTYPE = 'encryptedstring'; // modify to match your type name
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return base64_decode($value);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return base64_encode($value);
}
public function getName()
{
return self::MYTYPE;
}
}
I registered this type in my bundle class:
class MyOwnBundle extends Bundle
{
public function boot()
{
$em = $this->container->get("doctrine.orm.entity_manager");
try
{
Type::addType("encryptedstring", "My\OwnBundle\Type\EncryptedStringType");
$em->
getConnection()->
getDatabasePlatform()->
registerDoctrineTypeMapping("encryptedstring", "encryptedstring");
} catch (\Doctrine\DBAL\DBALException $e)
{
// For some reason this exception gets thrown during
// the clearing of the cache. I didn't have time to
// find out why :-)
}
}
}
and then I was able to reference it when creating my entities, eg:
/**
* #ORM\Column(type="encryptedstring")
* #Assert\NotBlank()
*/
protected $name;
This was a quick implementation, so I'd be interested to know the correct way of doing it. I presume also that your encryption service is something available from the container; I don't know how feasible/possible it would be to pass services into custom types this way either... :-)
richsage's answer was pretty good, except I wouldn't register the custom type in the bundle class file. It's recommended that you use the config.yml like so:
# ./app/config/confi
doctrine:
dbal:
driver: "%database_driver%"
{{ etc, etc }}
types:
encrypted_string: MyCompany\MyBundle\Type\EncryptedStringType
Then just make sure in your EncryptedStringType class you specify the getName function to return encrypted_string.
Now in your model definition (or annotation) you can use the encrypted_string type.