I just started using the dunglas api platform. Im using v2.0.0-rc1 and i added an custom operation to enabled/disable an user.
This is my custom action for the user
<?php
namespace Zoef\UserBundle\Action;
use Zoef\UserBundle\Entity\User;
use Doctrine\Common\Persistence\ManagerRegistry;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\Routing\Annotation\Route;
class UserAction
{
/**
* #Route(
* name="enabled_user",
* path="/users/{id}/enabled",
* defaults={"_api_resource_class"=User::class, "_api_item_operation_name"="enabled"}
* )
* #Method("PUT")
*/
public function __invoke(User $user)
{
if($user->isEnabled()) {
$user->setEnabled(false);
} else {
$user->setEnabled(true);
}
return $user;
}
}
When i go to my docs the custom operation is added and functional but to use this action i need to send 4 parameters: email, fullname, username, enabled. but i only want to send the enabled parameter and the id of the user is given in the route but i cant find in the doc how to change the parameters.
Can someone help me with this?
I was trying to make the same enable/disabled and I did it this way:
I created a custom controller in AppBundle\Controller\AddressController
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class AddressController extends Controller
{
public function enableAction($data)
{
$data->setActive(true);
$em = $this->getDoctrine()->getManager();
$em->persist($data);
$em->flush();
return $data;
}
}
In my routing.yml I have:
address_enable:
path: '/addresses/{id}/enable'
methods: ['PUT']
defaults:
_controller: 'AppBundle:Address:enable'
_api_resource_class: 'AppBundle\Entity\Address'
_api_item_operation_name: 'enable'
In my entity, I have:
* #ApiResource(
* itemOperations={
* "enable"={"route_name"="address_enable"},
* }
* )
And after that I just send it as URL/addresses/123/enable no need to send more parameters, just the id.
Related
Im trying to create an impersonate operation within my user controller, I have been following this guide..
impersonate for backpack
The setupImpersonateDefaults function gets called ok but i get a 404 error, after some testing i figured out the setupImpersonateRoutes is not getting triggered
Any ideas on why?
<?php
namespace App\Http\Controllers\Admin\Operations;
use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD;
use Illuminate\Support\Facades\Route;
use Session;
use Alert;
trait ImpersonateOperation
{
/**
* Define which routes are needed for this operation.
*
* #param string $segment Name of the current entity (singular). Used as first URL segment.
* #param string $routeName Prefix of the route name.
* #param string $controller Name of the current CrudController.
*/
protected function setupImpersonateRoutes($segment, $routeName, $controller)
{
Route::get($segment.'/{id}/impersonate', [
'as' => $routeName.'.impersonate',
'uses' => $controller.'#impersonate',
'operation' => 'impersonate',
]);
}
/**
* Add the default settings, buttons, etc that this operation needs.
*/
protected function setupImpersonateDefaults()
{
CRUD::allowAccess('impersonate');
CRUD::operation('impersonate', function () {
CRUD::loadDefaultOperationSettingsFromConfig();
});
CRUD::operation('list', function () {
// CRUD::addButton('top', 'impersonate', 'view', 'crud::buttons.impersonate');
CRUD::addButton('line', 'impersonate', 'view', 'crud::buttons.impersonate');
});
}
/**
* Show the view for performing the operation.
*
* #return Response
*/
public function impersonate()
{
CRUD::hasAccessOrFail('impersonate');
// prepare the fields you need to show
$this->data['crud'] = $this->crud;
$this->data['title'] = CRUD::getTitle() ?? 'Impersonate '.$this->crud->entity_name;
$entry = $this->crud->getCurrentEntry();
backpack_user()->setImpersonating($entry->id);
Alert::success('Impersonating '.$entry->name.' (id '.$entry->id.').')->flash();
// load the view
return redirect('dashboard');
// load the view
//return view('crud::operations.impersonate', $this->data);
}
}
Have tried following the guides and the routes are not getting added.
for anyone else looking at this, you need to call the route from the \routes\backpack\custom.php file, if its not called from this file it wont trigger the setupXXXRoute function
One of the official Backpack team members has created an add-on for impersonating users. You can use his add-on or get inspiration from it:
https://github.com/maurohmartinez/impersonate-users-backpack-laravel
New to Laravel and Backpack here, but trying to integrate the PermissionManager with Backpack. I've got it all installed and showing the Users/Permissions/Roles in the UI, however I was unable to figure out how to show/hide buttons and functionality in the Backpack UI based on those permissions. I'm hoping someone can comment on the solution I came up with or if there is something else that should be used.
Note, this is really about showing and hiding UI elements, not the actual policies (which I am handling separately using the "can" functions in my controllers, routes, etc.)
My solution:
In my EntityCrudController, I use a trait I made called CrudPermissionsLink, then in setup() I call the function I made:
public function setup()
{
CRUD::setModel(\App\Models\ProgramUnit::class);
CRUD::setRoute(config('backpack.base.route_prefix') . '/programunit');
CRUD::setEntityNameStrings('programunit', 'program_units');
$this->linkPermissions();
}
Then in my trait, I have it simply defined based on a naming convention, splitting on dashes.
<?php
namespace App\Http\Traits;
use Illuminate\Support\Facades\Auth;
/**
* Properties and methods used by the CrudPermissionsLink trait.
*/
trait CrudPermissionsLink
{
/**
* Remove access to all known operations by default, reset them based on permissions defined in the format
* entity_name-operation
*
*/
public function linkPermissions()
{
$ui_ops = ['list','create','delete','update'];
$user = Auth::user();
$this->crud->denyAccess($ui_ops);
foreach($ui_ops as $op){
$perm_name = "{$this->crud->entity_name}-{$op}";
if($user->can($perm_name)){
$this->crud->allowAccess($op);
}
}
}
}
What you have will work. That said, I recently created a similar solution for my apps. For my solution, I used an abstract Crud controller as below and all my specific crud controllers extend this class:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Model;
use Backpack\CRUD\app\Http\Controllers\Operations\ListOperation;
use Backpack\CRUD\app\Http\Controllers\Operations\CreateOperation;
use Backpack\CRUD\app\Http\Controllers\Operations\UpdateOperation;
use Backpack\CRUD\app\Http\Controllers\Operations\DeleteOperation;
use Backpack\CRUD\app\Http\Controllers\CrudController as BaseCrudController;
abstract class CrudController extends BaseCrudController
{
use ListOperation, DeleteOperation;
use CreateOperation { store as traitStore; }
use UpdateOperation { update as traitUpdate; }
/**
* All possible CRUD "actions"
*/
public const CRUD_ACTION_CREATE = 'create';
public const CRUD_ACTION_LIST = 'list'; // synonymous with "read"
public const CRUD_ACTION_UPDATE = 'update';
public const CRUD_ACTION_DELETE = 'delete';
public const CRUD_ACTION_REORDER = 'reorder';
public const CRUD_ACTION_REVISIONS = 'revisions';
/**
* #var array An array of all possible CRUD "actions"
*/
public const ACTIONS = [
self::CRUD_ACTION_CREATE,
self::CRUD_ACTION_LIST,
self::CRUD_ACTION_UPDATE,
self::CRUD_ACTION_DELETE,
self::CRUD_ACTION_REORDER,
self::CRUD_ACTION_REVISIONS,
];
/**
* #var array An array of all CRUD "actions" that are not allowed for this resource
* Add any of the CRUD_ACTION_X constants to this array to prevent users accessing
* those actions for the given resource
*/
public $_prohibitedActions = [
self::CRUD_ACTION_REORDER, // not currently using this feature
self::CRUD_ACTION_REVISIONS, // not currently using this feature
];
/**
* Protect the operations of the crud controller from access by users without the proper
* permissions
*
* To give a user access to the operations of a CRUD page give that user the permissions below
* (where X is the name of the table the CRUD page works with)
*
* `X.read` permission: users can view the CRUD page and its records
* `X.create` permission: users can create records on the CRUD page
* `X.update` permission: users can update records on the CRUD page
* `X.delete` permission: users can delete records on the CRUD page
* `X.reorder` permission: users can reorder records on the CRUD page
* `X.revisions` permission: users can manage record revisions on the CRUD page
*
* #return void
*/
public function setupAccess(): void
{
// get the name of the table the crud operates on
$table = null;
if (isset($this->crud->model) && $this->crud->model instanceof Model) {
/** #var Model $this->crud->Model; */
$table = $this->crud->model->getTable();
}
// for each action, check if the user has permissions
// to perform that action and enforce the result
foreach (self::ACTIONS as $action) {
$requiredPermission = "$table.$action";
// If our model has no $table property set deny all access to this CRUD
if ($table && !$this->isProhibitedAction($action) && Gate::check($requiredPermission)) {
$this->crud->allowAccess($action);
continue;
}
$this->crud->denyAccess($action);
}
}
/**
* Check if the given action is allowed for this resource
* #param string $action One of the CRUD_ACTION_X constants
* #return bool
*/
public function isProhibitedAction($action): bool
{
return in_array($action, $this->_prohibitedActions, true);
}
/**
* Setup the CRUD page
* #throws \Exception
*/
public function setup(): void
{
$this->setupAccess();
}
}
Is there any way to set the circular reference limit in the serializer component of Symfony (not JMSSerializer) with any config or something like that?
I have a REST Application with FOSRestBundle and some Entities that contain other entities which should be serialized too. But I'm running into circular reference errors.
I know how to set it like this:
$encoder = new JsonEncoder();
$normalizer = new ObjectNormalizer();
$normalizer->setCircularReferenceHandler(function ($object) {
return $object->getName();
});
But this has to be done in more than one controller (overhead for me).
I want to set it globally in the config (.yml) e.g. like this:
framework:
serializer:
enabled: true
circular_limit: 5
Found no serializer API reference for this so I wonder is it possible or not?
For a week have I been reading Symfony source and trying some tricks to get it work (on my project and without installing a third party bundle: not for that functionality) and I finally got one. I used CompilerPass (https://symfony.com/doc/current/service_container/compiler_passes.html)... Which works in three steps:
1. Define build method in bundle
I choosed AppBundle because it is my first bundle to load in app/AppKernel.php.
src/AppBundle/AppBundle.php
<?php
namespace AppBundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new AppCompilerPass());
}
}
2. Write your custom CompilerPass
Symfony serializers are all under the serializer service. So I just fetched it and added to it a configurator option, in order to catch its instanciation.
src/AppBundle/AppCompilerPass.php
<?php
namespace AppBundle;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class AppCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$container
->getDefinition('serializer')
->setConfigurator([
new Reference(AppConfigurer::class), 'configureNormalizer'
]);
}
}
3. Write your configurer...
Here, you create a class following what you wrote in the custom CompilerPass (I choosed AppConfigurer)... A class with an instance method named after what you choosed in the custom compiler pass (I choosed configureNormalizer).
This method will be called when the symfony internal serializer will be created.
The symfony serializer contains normalizers and decoders and such things as private/protected properties. That is why I used PHP's \Closure::bind method to scope the symfony serializer as $this into my lambda-like function (PHP Closure).
Then a loop through the nomalizers ($this->normalizers) help customize their behaviours. Actually, not all of those nomalizers need circular reference handlers (like DateTimeNormalizer): the reason of the condition there.
src/AppBundle/AppConfigurer.php
<?php
namespace AppBundle;
class AppConfigurer
{
public function configureNormalizer($normalizer)
{
\Closure::bind(function () use (&$normalizer)
{
foreach ($this->normalizers as $normalizer)
if (method_exists($normalizer, 'setCircularReferenceHandler'))
$normalizer->setCircularReferenceHandler(function ($object)
{
return $object->getId();
});
}, $normalizer, $normalizer)();
}
}
Conclusion
As said earlier, I did it for my project since I dind't wanted FOSRestBundle nor any third party bundle as I've seen over Internet as a solution: not for that part (may be for security). My controllers now stand as...
<?php
namespace StoreBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class ProductController extends Controller
{
/**
*
* #Route("/products")
*
*/
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$data = $em->getRepository('StoreBundle:Product')->findAll();
return $this->json(['data' => $data]);
}
/**
*
* #Route("/product")
* #Method("POST")
*
*/
public function newAction()
{
throw new \Exception('Method not yet implemented');
}
/**
*
* #Route("/product/{id}")
*
*/
public function showAction($id)
{
$em = $this->getDoctrine()->getManager();
$data = $em->getRepository('StoreBundle:Product')->findById($id);
return $this->json(['data' => $data]);
}
/**
*
* #Route("/product/{id}/update")
* #Method("PUT")
*
*/
public function updateAction($id)
{
throw new \Exception('Method not yet implemented');
}
/**
*
* #Route("/product/{id}/delete")
* #Method("DELETE")
*
*/
public function deleteAction($id)
{
throw new \Exception('Method not yet implemented');
}
}
The only way I've found is to create your own object normalizer to add the circular reference handler.
A minimal working one can be:
<?php
namespace AppBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
class AppObjectNormalizer extends ObjectNormalizer
{
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)
{
parent::__construct($classMetadataFactory, $nameConverter, $propertyAccessor, $propertyTypeExtractor);
$this->setCircularReferenceHandler(function ($object) {
return $object->getName();
});
}
}
Then declare as a service with a slithly higher priority than the default one (which is -1000):
<service
id="app.serializer.normalizer.object"
class="AppBundle\Serializer\Normalizer\AppObjectNormalizer"
public="false"
parent="serializer.normalizer.object">
<tag name="serializer.normalizer" priority="-500" />
</service>
This normalizer will be used by default everywhere in your project.
I’m developing a form in zf2 and I want to calculate a value based upon user input and set it in a field after the form has validated. In the form, there is a firstName field and a lastName field; and I want to use the validated input to calculate a value to populate in a fullName field.
I assume I want to set the value something like this, but haven’t found the right code for setting the "element" that gets sent to the database:
public function addAction()
{
$objectManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
$form = new AddMemberForm($objectManager);
$member = new Member();
$form->bind($member);
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
// develop full name string and populate the field
$calculatedName = $_POST['firstName'] . " " . $_POST['lastName'];
$member->setValue('memberFullName', $calculatedName);
$this->getEntityManager()->persist($member);
$this->getEntityManager()->flush();
return $this->redirect()->toRoute('admin-members');
}
}
return array('form' => $form);
}
Doctrine's built-in Lifecycle Callbacks are perfectly fits for handling such requirement and I strongly recommend to use them.
You just need to correctly annotate the entity.
For example:
<?php
/**
* Member Entity
*/
namespace YourNamespace\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Event\LifecycleEventArgs;
/**
* #ORM\Table(name="members")
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*/
class Member
{
// After all of your entity properies, getters and setters... Put the method below
/**
* Calculate full name on pre persist.
*
* #ORM\PrePersist
* #return void
*/
public function onPrePersist(LifecycleEventArgs $args)
{
$this->memberFullName = $this->getFirstName().' '.$this->getLastName();
}
}
With this way, memberFullName property of the entity will be automatically populated using first and last names on entity level just before persisting.
Now you can remove the lines below from your action:
// Remove this lines
$calculatedName = $_POST['firstName'] . " " . $_POST['lastName'];
$member->setValue('memberFullName', $calculatedName);
Foozy’s excellent answer provides a solution that works for an add action, and the response to my comment directed me to the following solution that works for both add and edit actions:
<?php
/**
* Member Entity
*/
namespace YourNamespace\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Event\PreFlushEventArgs;
/**
* #ORM\Table(name="members")
* #ORM\Entity
*/
class Member
{
// After all of your entity properies, getters and setters... Put the method below
/**
* Calculate full name on pre flush.
*
* #ORM\PreFlush
* #return void
*/
public function onPreFlush(PreFlushEventArgs $args)
{
$this->memberFullName = $this->getFirstName().' '.$this->getLastName();
}
}
I'm trying to make an API Rest in Symfony2 using Parse as cloud database.
If I try to retrieve the Parse users it works fine and returns the expected data.
Local url example: http://www.foo.local/app_dev.php/getUsers/
Here is the code I use in the Users controller (I use annotations in order to set the routes in the controller):
namespace Foo\ApiBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use FOS\RestBundle\Controller\Annotations\View;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use Parse\ParseClient;
use Parse\ParseObject;
use Parse\ParseQuery;
use Parse\ParseUser;
class UsersController extends Controller
{
/**
* #return array
* #View()
* #Route("/getUsers/")
*/
public function getUsersAction(Request $request) {
ParseClient::initialize(<my Parse keys>);
$query = ParseUser::query();
$results = $query->find();
return array('users' => $results);
}
}
However if I try the same with my Products ParseObjects, I get the following error message:
error code="500" message="Internal Server Error" exception
class="Doctrine\Common\Annotations\AnnotationException"
message="[Semantical Error] The annotation "#returns" in method
Parse\ParseFile::getData() was never imported. Did you maybe forget to
add a "use" statement for this annotation?"
Local url example: http://www.foo.local/app_dev.php/getProducts/
The Products controller code:
namespace Foo\ApiBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use FOS\RestBundle\Controller\Annotations\View;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use Parse\ParseClient;
use Parse\ParseObject;
use Parse\ParseQuery;
use Parse\ParseUser;
use Parse\ParseFile;
class ProductsController extends Controller
{
/**
* #return array
* #View()
* #Route("/getProducts/")
*/
public function getProductsAction(Request $request) {
ParseClient::initialize(<my Parse keys>);
$query = new ParseQuery("Products");
$results = $query->find();
return array('products' => $results);
}
}
If instead of returning $results I return other dummy data, like return array('products' => 'fooProducts'), I no longer get the error message.
Also if I make a var_dump of the $results variable, I get the expected array of ParseObjects.
Here is my routing.yml file in case there is something wrong with it:
api:
resource: "#FooApiBundle/Controller/"
type: annotation
prefix: /
users:
type: rest
resource: Foo\ApiBundle\Controller\UsersController
products:
type: rest
resource: Foo\ApiBundle\Controller\ProductsController
By the error message it seems that the problem is related to Doctrine, but since I'm not using it, I don't know exactly how there can be a conflict or how to fix it. Any suggestions?
There are a few DocBlock typos of #returns in the Parse\ParseFile class that is causing Doctrine's Annotations class to attempt to identify them as a class. This is not your fault but a bug in the Parse PHP SDK library.
I've made a fix in this commit and submitted a pull request back to the original devs, so it should be a simple matter of eventually running composer update to bring your Parse library to the latest correct version.
You can read more about DocBlock and the part specifically on Annotations here
Here is a copy/paste of the resulting diff for src/Parse/ParseFile.php:
## -31,7 +31,7 ## class ParseFile implements \Parse\Internal\Encodable
/**
* Return the data for the file, downloading it if not already present.
*
- * #returns mixed
+ * #return mixed
*
* #throws ParseException
*/
## -50,7 +50,7 ## public function getData()
/**
* Return the URL for the file, if saved.
*
- * #returns string|null
+ * #return string|null
*/
public function getURL()
{
## -112,7 +112,7 ## public function getMimeType()
* #param string $name The file name on Parse, can be used to detect mimeType
* #param string $mimeType Optional, The mime-type to use when saving the file
*
- * #returns ParseFile
+ * #return ParseFile
*/
public static function createFromData($contents, $name, $mimeType = null)
{
## -132,7 +132,7 ## public static function createFromData($contents, $name, $mimeType = null)
* #param string $name Filename to use on Parse, can be used to detect mimeType
* #param string $mimeType Optional, The mime-type to use when saving the file
*
- * #returns ParseFile
+ * #return ParseFile
*/
public static function createFromFile($path, $name, $mimeType = null)
{
The correct way to initialize Parse using Symfony, is on the setContainer method of your controller:
class BaseController extends Controller
{
....
public function setContainer(ContainerInterface $container = null)
{
parent::setContainer( $container );
ParseClient::initialize( $app_id, $rest_key, $master_key );
}
}
Depending of your needs, you can create a BaseController and extend it in your rest of controllers.
class UsersController extends Controller
In addition, you could add your keys in the parameters.yml file.
parameters:
#your parameters...
ParseAppId: your_id
ParseRestKey: your_rest_key
ParseMasterKey: your_master_key
TIP: Note you can add have different Parse projects (dev and release
version). Add your parameters in your different parameters
configuration provides an easy way to handle this issue.
class BaseController extends Controller
{
....
public function setContainer(ContainerInterface $container = null)
{
parent::setContainer( $container );
$app_id = $container->getParameter('ParseAppId');
$rest_key = $container->getParameter('ParseRestKey');
$master_key = $container->getParameter('ParseMasterKey');
ParseClient::initialize( $app_id, $rest_key, $master_key );
}
}