How to do proper error handling in services - symfony

I am writing a small service for a Symfony 4 project
and I'm just wondering if my error handling is a good idea.
My class currently looks like this:
namespace App\Service;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Exception\;
/**
* Class XMLHelper
* #package App\Service
*/
class XMLHelper
{
public function getContent(string $path): array
{
$fileSystem = new Filesystem();
if ($fileSystem->exists($path)) {
libxml_use_internal_errors(true);
$object = simplexml_load_file($path);
if ($object === false) {
$str = 'Failed loading XML: ';
foreach(libxml_get_errors() as $error) {
$str = $error->message . ', ';
}
throw new \UnexpectedValueException($str);
}
return $object;
} else {
throw new \FileNotFoundException('File ' . $path . ' not found.');
}
}
}
My question is, do I need that at all? Will symfony not throw
out an error anyway. Is the output not superfluous? How do
you do it the best or generally correctly? If you have own
Exceptions in Services.

IMO you should never hesitate to throw an exception when appropriate. Your code looks just fine, it provides an exhaustive information about the encountered exeptions.
There is nothing to do with Symfony ifself, it up to your code to throw right exceptions in right places.
Hope this helps.
P.S. And don't forget to libxml_clear_errors();

Related

Symfony CMF - Uploading Images and PDF Files

I am using the Symfony CMF Media Bundle to achieve the following. I am having several nodes that can have an image and a downloadable PDF.
I have already figured out that the setImage method has to be implemented like that:
public function setPreviewImage($previewImage)
{
if ($previewImage === null) {
return $this;
}
if (!$previewImage instanceof ImageInterface && !$previewImage instanceof UploadedFile) {
$type = is_object($previewImage) ? get_class($previewImage) : gettype($previewImage);
throw new \InvalidArgumentException(sprintf(
'Image is not a valid type, "%s" given.',
$type
));
}
if ($this->previewImage) {
$this->previewImage->copyContentFromFile($previewImage);
} elseif ($previewImage instanceof ImageInterface) {
$previewImage->setName('previewImage');
$this->previewImage = $previewImage;
} else {
$this->previewImage = new Image();
$this->previewImage->copyContentFromFile($previewImage);
}
return $this;
}
Then in another forum someone was suggested to make this property cascade-persistent. with that hint: https://github.com/symfony-cmf/BlockBundle/blob/master/Resources/config/doctrine-phpcr/ImagineBlock.phpcr.xml#L22. Now i am wondering how and were i can set this option in my configuration.
The next part i am wondering about is the cmf_media_file type. Has anyone out here ever managed to store a PDF into a PHPCR node property?
For any help i would be really thankful.
I figured it out by myself.
For anyone who is using annotations you have to set it up like this:
use Symfony\Cmf\Bundle\MediaBundle\Doctrine\Phpcr\Image;
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR;
/**
* #var Image
* #PHPCR\Child(cascade="persist")
*/

Whats the best way to override object deletion in Sonata Admin?

I already have a custom CRUD controller. So do I just need to override Controller::deleteAction() and Controller::batchDeleteAction() from Sonata\AdminBundle\Controller ?
Or is it preferable / better practice to override the Admin class's delete methods?
My desired behaviour is that I want to update a record with an archived flag rather than delete the entity.
The docs are incomplete on this subject
Update
The following code in my Entity's Repository class iterates over a query object as per the batchDelete method in the ModelManager
public function batchArchive($class, ProxyQuery $queryProxy)
{
$queryProxy->select('DISTINCT '.$queryProxy->getRootAlias());
try {
$entityManager = $this->getEntityManager();
$batchSize = 20;
$i = 0;
foreach ($queryProxy->getQuery()->iterate() as $pos => $object) {
$this->archiveMyEntity($object); //???
if (($i % $batchSize) == 0) {
$entityManager->flush();
$entityManager->clear();
}
++$i;
}
} catch (\PDOException $e) {
throw new ModelManagerException('', 0, $e);
} catch (DBALException $e) {
throw new ModelManagerException('', 0, $e);
}
}
The problem I have is that the object my archiveMyEntity() method expects is an Entity object not a query object.
I overwrote the delete logic in the admin class than in my custom CRUD controller I overwrote the batchActionDelete logic with the following:
public function batchActionDelete(\Sonata\AdminBundle\Datagrid\ProxyQueryInterface $query)
{
if (false === $this->admin->isGranted('DELETE')) {
throw new AccessDeniedException();
}
$res = $query->execute();
if (count($res)) {
foreach ($res as $sqTeamEntity) {
$this->admin->delete($sqTeamEntity, false);
}
$this->admin->flushDoctrine(); //custom method in the admin class
$this->addFlash('sonata_flash_success', 'flash_batch_delete_success');
}
return new RedirectResponse(
$this->admin->generateUrl('list',
$this->admin->getFilterParameters())
);
}
So I fetch all the entities and just call the delete method from the admin class.
Hope this helps.
If you wish to override the controller logic or view, overriding the methods you indicated is the way to go.
However, if your objective is to perform custom logic before or after the deletion, you may override the Admin::preRemove($object) or Admin::postRemove($object) methods.
You may as well override the whole logic by overriding the Admin::delete($object) method.
Feel free to submit a PR - or comment in the related Github issue - to improve the docs as well.

Laravel 4 Model Events don't work with PHPUnit

I build a model side validation in Laravel 4 with the creating Model Event :
class User extends Eloquent {
public function isValid()
{
return Validator::make($this->toArray(), array('name' => 'required'))->passes();
}
public static function boot()
{
parent::boot();
static::creating(function($user)
{
echo "Hello";
if (!$user->isValid()) return false;
});
}
}
It works well but I have issues with PHPUnit. The two following tests are exactly the same but juste the first one pass :
class UserTest extends TestCase {
public function testSaveUserWithoutName()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // pass
assertEquals($count, User::all()->count()); // pass
}
public function testSaveUserWithoutNameBis()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // fail
assertEquals($count, User::all()->count()); // fail, the user is created
}
}
If I try to create a user twice in the same test, it works, but it's like if the binding event is present only in the first test of my test class. The echo "Hello"; is printed only one time, during the first test execution.
I simplify the case for my question but you can see the problem : I can't test several validation rules in different unit tests. I try almost everything since hours but I'm near to jump out the windows now ! Any idea ?
The issue is well documented in Github. See comments above that explains it further.
I've modified one of the 'solutions' in Github to automatically reset all model events during the tests. Add the following to your TestCase.php file.
app/tests/TestCase.php
public function setUp()
{
parent::setUp();
$this->resetEvents();
}
private function resetEvents()
{
// Get all models in the Model directory
$pathToModels = '/app/models'; // <- Change this to your model directory
$files = File::files($pathToModels);
// Remove the directory name and the .php from the filename
$files = str_replace($pathToModels.'/', '', $files);
$files = str_replace('.php', '', $files);
// Remove "BaseModel" as we dont want to boot that moodel
if(($key = array_search('BaseModel', $files)) !== false) {
unset($files[$key]);
}
// Reset each model event listeners.
foreach ($files as $model) {
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Reregister them.
call_user_func(array($model, 'boot'));
}
}
I have my models in subdirectories so I edited #TheShiftExchange code a bit
//Get all models in the Model directory
$pathToModels = '/path/to/app/models';
$files = File::allFiles($pathToModels);
foreach ($files as $file) {
$fileName = $file->getFileName();
if (!ends_with($fileName, 'Search.php') && !starts_with($fileName, 'Base')) {
$model = str_replace('.php', '', $fileName);
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Re-register them.
call_user_func(array($model, 'boot'));
}
}

Symfony2 set class variable with init or construct methods

Have recently been using Symfony2 after using ZF for some time.
I am having problems trying to do something relatively simple, I think.
The following code is within a controller:
private $current_setid = "";
public function __construct() {
$current_set = $this->getCurrentSet();
if ($current_set == "") {
return $this->redirect($this->generateUrl('selectset'));
}
$this->current_setid = $current_set;
}
public function getCurrentSet() {
$session = $this->get("session");
$set = $session->get('set');
return $set;
}
public function setCurrentSet($setid) {
$session = $this->get("session");
$session->set('set', "$setid");
}
If I use __construct() I get errors like:
Fatal error: Call to a member function get() on a non-object in
I have tried using __init() and init() both of which do not seem to get called.
Can anyone point me in the right direction? Is there a simple way to do this or do I have to look into event listeners?
Have you tried getting your session like they do in official documentation?
$session = $this->getRequest()->getSession();
$foo = $session->get('foo');
Basically get fetch dependencies from container and container in the Controller is injected using setter dependency injection. You just not have container in the time of __construct yet.
Just ended up opting for placing a check in every method in the class. Seems silly to have to do that but I find I often have to do that in Symfony2 with the lack of init, postDispatch type methods like ZF has.
Even trying to remove the check to another method was counter productive as I still had to check the return from that method as $this->redirect does not seem to work unless it is within an Action method. For example:
public function isSetSet() {
$current_set = $this->getCurrentSet();
if ($current_set == "") {
$url = $this->generateUrl('selectset');
return $this->redirect($url);
}
return TRUE;
}
public function someAction() {
$check = $this->isSetSet();
if($check != TRUE){
return $check;
}
...
}
So each method needs that 4 line check but the whole check can be done in 4 lines anyway so no need for that extra method:
public function anotherAction() {
$current_setid = $this->getCurrentSet();
if ($current_setid == "") {
return $this->redirect($this->generateUrl('selectset'));
}
...
}

Symfony2 data transformer, validator and error message

I asked this question and found out that we can't get the error message thrown by a DataTransformer (according to the only user who answered, maybe it's possible, I don't know).
Anyway, now that I know that, I am stucked with a problem of validation. Suppose my model is this one: I have threads that contains several participants (users).
<?php
class Thread
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="My\UserBundle\Entity\User")
* #ORM\JoinTable(name="messaging_thread_user")
*/
private $participants;
// other fields, getters, setters, etc
}
For thread creation, I want the user to specify the participants usernames in a textarea, separated by "\n".
And I want that if one or more of the usernames specified don't exist, a message is displayed with the usernames that don't exist.
For example, "Users titi, tata and toto don't exist".
For that I created a DataTransformer that transforms the raw text in the textarea into an ArrayCollection containing instances of users. Since I can't get the error message provided by this DataTransformer (such a shame! Is it really impossible?), I don't check the existence of each usernames in the DataTransformer but in the Validator.
Here is the DataTransformer that converts \n-separated user list into an ArrayCollection (so that the DataBinding is ok):
<?php
public function reverseTransform($val)
{
if (empty($val)) {
return null;
}
$return = new ArrayCollection();
// Extract usernames in an array from the raw text
$val = str_replace("\r\n", "\n", trim($val));
$usernames = explode("\n", $val);
array_map('trim', $usernames);
foreach ($usernames as $username) {
$user = new User();
$user->setUsername($username);
if (!$return->contains($user)) {
$return->add($user);
}
}
return $return;
}
And here is my validator:
<?php
public function isValid($value, Constraint $constraint)
{
$repo = $this->em->getRepository('MyUserBundle:User');
$notValidUsernames = array();
foreach ($value as $user) {
$username = $user->getUsername();
if (!($user = $repo->findOneByUsername($username))) {
$notValidUsernames[] = $username;
}
}
if (count($notValidUsernames) == 0) {
return true;
}
// At least one username is not ok here
// Create the list of usernames separated by commas
$list = '';
$i = 1;
foreach ($notValidUsernames as $username) {
if ($i < count($notValidUsernames)) {
$list .= $username;
if ($i < count($notValidUsernames) - 1) {
$list .= ', ';
}
}
$i++;
}
$this->setMessage(
$this->translator->transChoice(
'form.error.participant_not_found',
count($notValidUsernames),
array(
'%usernames%' => $list,
'%last_username%' => end($notValidUsernames)
)
)
);
return false;
}
This current implementation looks ugly. I can see the error message well, but the users in the ArrayCollection returned by the DataTransformer are not synchronized with Doctrine.
I got two questions:
Is there any way that my validator could modify the value given in parameter? So that I can replace the simple User instances in the ArrayCollection returned by the DataTransformer into instances retrieved from the database?
Is there a simple and elegant way to do what I'm doing?
I guess the most simple way to do this is to be able to get the error message given by the DataTransformer. In the cookbook, they throw this exception: throw new TransformationFailedException(sprintf('An issue with number %s does not exist!', $val));, if I could put the list of non-existing usernames in the error message, it would be cool.
Thanks!
I am the one that answered your previous thread so maybe someone else will jump in here.
Your code can be simplified considerably. You are only dealing with user names. No need for use objects or array collections.
public function reverseTransform($val)
{
if (empty($val)) { return null; }
// Extract usernames in an array from the raw text
// $val = str_replace("\r\n", "\n", trim($val));
$usernames = explode("\n", $val);
array_map('trim', $usernames);
// No real need to check for dups here
return $usernames;
}
The validator:
public function isValid($userNames, Constraint $constraint)
{
$repo = $this->em->getRepository('SkepinUserBundle:User');
$notValidUsernames = array();
foreach ($userNames as $userName)
{
if (!($user = $repo->findOneByUsername($username)))
{
$notValidUsernames[$userName] = $userName; // Takes care of dups
}
}
if (count($notValidUsernames) == 0) {
return true;
}
// At least one username is not ok here
$invalidNames = implode(' ,',$notValidUsernames);
$this->setMessage(
$this->translator->transChoice(
'form.error.participant_not_found',
count($notValidUsernames),
array(
'%usernames%' => $invalidNames,
'%last_username%' => end($notValidUsernames)
)
)
);
return false;
}
=========================================================================
So at this point
We have used transformer to copy the data from the text area and generated an array of user names during form->bind().
We then used a validator to confirm that each user name actually exists in the database. If there are any that don't then we generate an error message and form->isValid() will fail.
So now we are back in the controller, we know we have a list of valid user names (possibly comma delimited or possibly just an array). Now we want to add these to our thread object.
One way would to create a thread manager service and add this functionality to it. So in the controller we might have:
$threadManager = $this->get('thread.manager');
$threadManager->addUsersToThread($thread,$users);
For the thread manager we would inject our entity manager. In the add users method we would get a reference to each of the users, verify that the thread does not already have a link to this user, call $thread->addUser() and then flush.
The fact that we have wrapped up this sort of functionality into a service class will make things easier to test as we can also make a command object and run this from the command line. it also gives us a nice spot to add additional thread related functionality. We might even consider injecting this manager into the user name validator and moving some of the isValid code to the manager.

Resources