Symfony URL organization - symfony

A project I am working on contains a search feature. I would like users to have the ability to bookmark a page along with their query so they can bookmark their search page.
In the traditional implementation, I simply used GET parameters in the URL.
However, with symfony I am having a hard time understanding what is best to use for this functionality? is it just a matter of preference?
Should I just create a controller such as:
search/{query}/{page}
and when a user clicks on an option
description/{id}
or should I implement the traditional get variable in the URL:
Note the URL's on Stack Overflow, for example. When you search for something and you navigate the pages, the URL is something like:
search?q="nav&page=2 But then when you click on a post the URL becomes something like questions/19157969/expanded-navigation-by-default, so it is kind of a mix of the two.
Couldn't SO use something like search/search-term-here/2 for the same functionality? is there any benefit for picking GET over the alternative, using Symfony?
I appreciate any suggestions! many thanks in advance!

It is a matter of preference. Some say that the URL is more important than the query string for SEO purposes. Also, /search/{query}/{page} is better looking in general, but that's just my opinion.

Search terms can get very long. Perhaps your searching mechanism is fairly simple, but personally I wouldn't want to make that assumption, especially if you plan on using the same feature for something in future you haven't designed the scale of yet.
Using a typical GET string is safe enough in my opinion, the controller should know what variables it needs, and GET queries generally don't (shouldn't) have an impact on routing.
However if you want to achieve pretty search URLs which can always be revisited, you could try storing each search in a database.
An example (untested):
class SearchController extends Controller
{
/**
* #Route("/search" name="search")
* #Method("GET")
*/
public function searchAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$search = new Search();
$search->setQuery($request->query);
// The query field would be of an object type in this example,
// but you could store the data any way you want really.
// Add any additional information about the search/request to the entity.
$em->persist($search);
$em->flush();
$this->redirect($this->generateUrl('search_results', array(
'search_id' => $search->getId()
)));
}
/**
* #Route("/search/{search_id}", name="search_results")
* #Method("GET")
* #Template()
*/
public function resultsAction($search_id)
{
$em = $this->getDoctrine()->getManager();
if(! $search = $em->getRepository('AcmePostBundle:Search')) {
throw $this->createNotFoundException();
}
$query = $search->getQuery(); // This will be a ParameterBag
// Run your search filters
return array(
// Your search results
);
}
}
In this example the request is to an action with a GET string as normal (could be POST if you prefer), which saves the search data and then redirects to an action which gets the search data from the database, does its filtering and show the results.
I can't see any real performance loss in a technique like this, the database queries to insert and select the search should be relatively small, but I can see the table in which you store them getting very full very quickly.
Then again it is always useful (occasionally) to draw reports on popular search terms.

Related

What's the recommended way in Symfony of taking JSON data and populating an entity?

If I'm receiving JSON data in a request (say an API type interface), what's the Symfony recommended way of populating an entity. It seems to me the options are:
Use the form component – passing the decoded JSON as an array to the submit() method.
Use the Serializer to deserialize.
It seems to me the issue with using the serializer is that you need to manually do data transformation (and validation, though simple).
Using the form component feels kind of hacky and a also uses a lot of functionality that isn't touched/needed.
Are there other options (built into Symfony or other bundles/packages)? Or is one of these the recommended way?
(I realize this is at least partially an opinion based question...)
As you've mentioned - this is a pretty opinionated issue to deal with. The options you've been considering are the two common ways of handling it:
Just go with the Form component - need to create a FormType, adds some performance overhead (not significant in most cases). As a bonus - it gives you all the Form perks like not allowing extra fields, ability to use Form events etc.
Use Serializer + Validator - a "skinny" option in terms of components employed, a bit more verbose, doesn't come with Form perks
I'd say that there is really nothing wrong with using forms to handle deserialization and validation in one go.
Have a look at the sample action code below. Note that it is using the FOSRestBundle View class to handle responses. It simply takes in a json encoded entity data and either creates a new entity based on it or updates an existing one.
public function sampleAction(SampleEntity $sampleEntity, Request $request) {
//Is it a new or existing entity?
$statusCode = $sampleEntity->getId() ? 200 : 201;
//Load our form with the entity provided by the route loader
$form = $this->createForm(SampleEntityType::class, $sampleEntity);
//Decode the actual input and make Form component to populate an entity for us
$formData = json_decode($request->getContent(), true);
$form->submit($formData);
//Validation is as simple as this
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($sampleEntity);
$em->flush();
return View::create($form, $statusCode);
}
return View::create($form->getErrors(true, false), 400);
}

how to create save actions in api-platform

I'm trying to implement in my symfony project some api. Currently the project have many controller with standard crud, based on html table, form/validator etc.
I'm looking to the api-platform project that seam to make very easy the construction of standard rest api, and for the GET part it fit my necessities.
But for the POST/PUT/DELETE part it seam a very basic persist action on an entity, and suddenly in my project, i need to do many more actions after the persist of the entity.
I've red the docs and I'm really confused on how to do that...
I see two possibilities:
Using the event system, subscribing for the POST_WRITE for every entities
Creating a custom action for every create/update/delete actions of an entity
In both the case, I would have a really high number of single actions or event subscriber in the project (30/40), and it's really unconfortable to maintain. Also I probably have to replicate the same code that I already have in the controller, to maintain the old form system until is all rewitten in an API format.
Any suggestion on how to approach this problem?
There isn't a way to use the same controller actions, like in the FOSRestBundle, so that I can receive the data, do the various validation/persist/extra actions, and then return a result that is managed by the api-platform events?
Any way to manually call some part of the api-platform, like the deserialization/serialization, the filter and pagination from a standard controller action?
Thanks to all
Cheers
Daniele
Forgive me if I do not completely understand your question but if you already have the functionality written in the controller and you want to access the same actions via an api then maybe you can set multiple routes on each action and depending on how the action was called you can respond differently. For example:
/**
* #Route("/api/v1/tester", name="api_tester")
* #Route("/tester", name="tester")
*/
public function testerAction( Request $request )
{
$route = $request->attributes->get('_route');
if( $route == "api_tester" )
#..do things the api way
response = array( "success" => 1, "data" => $return_string );
return new Response( json_encode( $response ) );
} else { //non-api
$this->render('tester/basic.html.twig', array();
}
}
You can evaluate which route was used and in various sections of your actions you can handle things differently based on if the action was called via the api or from a normal request.

Symfony2 best way of removing business logic from controller and correct usage of model

I'm in searching of the best way of removing business logic from controller and correct usage of model(and maybe services).
Some details below.
Actually, my project is more complicated, but as example I will use Simple Blog application.
I have created my application (Simple Blog) in next steps:
created bundle
generated entities(Topic, Post, Comment)
generated controller for each entity, using doctrine:generate:crud
installed FOSUserBundle and generated User entity
So, I have all needed methods and forms in my controllers. But now I have some troubles:
Admin need to be able see all topics and posts, when simple User can only see
topic and posts where he is owner.
Currently there are indexAction, that return findAll common for any user. As solution, I can check in action, if ROLE_USER or ADMIN and return find result for each condition. But this variant keep some logic at action.
I also can generate action for each role, but what happened if roles amount will increase?
What is the best way to solve this problem with result for each role?
I need to edit some parameters before saving.
For example, I have some scheduler, where I create date in some steps, using features of DateTime.
Before saving I need to do some calculations with date.
I can do it in controller using service or simple $request->params edit.
What is the best way to edit some $request parameters before saving?
My questions I have marked with bold.
Thanks a lot for any help!
What I would do is to create a query which fetches the topics. Afterwards I would have a method argument which specifies if the query should select only the topics for a certain user or all topics. Something like this should do the work in your TopicRepository:
public function findTopics($userId = false)
{
$query = $this->createQueryBuilder('topic');
if($userId) {
$query->join('topic.user', 'user')
->where('user.id = :user_id')
->setParameter(':user_id', $userId)
;
}
return $query->getQuery()->getResult();
}
So, whenever you need to get the topics only by a user, you would pass a $userId to the method and it would return the results only for that user. In your controller you'd have something similar to this code (Symfony 2.6+):
$authorizationChecker = $this->get('security.authorization_checker');
if($authorizationChecker->isGranted('ROLE_ADMIN')){
$results = $this->get('doctrine.orm.entity_manager')->getRepository('TopicRepository')->findTopics();
} else {
$results = $this->get('doctrine.orm.entity_manager')->getRepository('TopicRepository')->findTopics($this->getUser()->getId());
}
You can try using Doctrine Events and create a PreUpdate depending on your case. See the documentation for more information. If you have a TopicFormType, you could also try the form events.
You are not supposed to "edit" a $request, which is why you can't directly do that. You can, however, retrieve a value, save it as a $variable and then do whatever you want with it. You can always create a new Request if you really need it. Could you be more specific what you want to do here and why is this necessary?

Symfony 2: filtering x-to-many relations

I have two entities that represent users (User) and friendship requests (FriendshipRequest). There is a oneToMany relationship between User and FriendshipRequest, so Doctrine creates a method that is called getFriendshipRequests() in the class User. This is ok, but FriendshipRequest has an attribute that is called status, so I would like that the User class could filter the friendship requests associated to it attending to their status. I have read Doctrine documentation, and I found out this:
Natively you can’t filter associations in 2.0 and 2.1. You should use
DQL queries to query for the filtered set of entities.
According to this, I suppose that I should create a FriendshipRequest repository and create a method called "findByStatusAndUser" (or something like that), but I think that's a crappy solution.
I would like to have a method in the User entity, like getPendingStatusRequests(). Is this possible? If it isn't, what would be the best solution?
As of Doctrine 2.3 you can use matching and Criteria.
Then you could use getPendingStatusRequests() in User entity just like you wanted.
For your example the code would look like this:
public function getPendingStatusRequests()
{
$criteria = Criteria::create(); //don't forget to use Doctrine\Common\Collections\Criteria;
$criteria->where(Criteria::expr()->eq('status', 1));
return $this->friendshipRequests->matching($criteria);
}
I think that "getPendingRequestsForUser($user)" method in the FriendshipRequest repository should be a good solution. Inside this method you just need to create an appropriate DQL.
This is a good solution, because all of the logic should be moved to repositories, leaving entities as small and clean as possible.
UPD: Also, you could use findBy method, as described here, ex:
$pendingRequests = $em->getRepository('MyBundle:FriendshipRequest')->findBy(
array('user' => $user->getId(), 'status' => 1)
);
But for me, first method is preferred.
You can certainly add getPendingStatusRequests() to user and then have it cycle through all the friendship requests and only return those with the appropriate status.
The only potential problem is that all of the friendship requests will always be loaded including those you don't need. It is up to you to decide if this is a real problem or not. It might be that once a friendship request is processed then it is removed so a user won't have many requests at any given time.
If you do want to avoid loading all the requests then make a query and use the WITH expression on your join clause. Something like:
$qb->leftJoin('user.friendshipRequests','request',
Expr\Join::WITH, $qb->expr()->eq('request.status', $qb->expr()->literal('Pending')));
And since you are using S2 I would not fool around with repositories. Just make a service called UserManager, inject the entity manager, and give it a method called loadUserWithPendingFriendshipRequests.

Drupal node creation - back to origin after creation

I have organic groups setup and within those group users are allowed to post certain content.
What I woulkd like to do is, when you create a node inside an organic group, it automatically defaults back to frontpage of the group, or the same page that I used to create the node.
At present it defaults to the node view page.I assume there must be a way to add some kind of code so that after the node creation it defauls back to its origin. I.E. the page from where the node was created from.
thanks :)
UPDATE: Got the below, but not entirly sure how to ensure that it redirects back to the GROUP node, from where it was created,
<?php
/**
* Grabs current node ID
*/
$node_nid = nid;
/**
* Implements hook_form_alter().
*/
function mod_form_alter(&$form, $form_state) {
$form['buttons']['submit']['#submit'][] = 'mod_form_finish_redirect';
unset($form['buttons']['preview']);
}
/**
* Custom submit handler. Overwrites the form redirection variable.
*/
function mod_form_finish_redirect($form, &$form_state) {
$form_state['redirect'] = '/content/<?php print $node_nid; ?>';
}
?>
I would recommend the rules module. Rules is a great module that allows you to do many kinds of workflow and it is perfect for this. You can write a rule that triggers when a specified node type is created (and include any other conditions you require as well). After the node is created you can specify a redirect action rule to the home page. This can all be done without any code.
Rules (as 'We Love Drupal' says) is a possibility, but also quite a big module for such a small change in behavior. Another option is to write a custom module implementing hook_form_alter setting the #redirect value of the form.
Keep in mind that of seeing the node you have just created is important feedback for a user. When you perform an action, you want confirmation that you have achieved your task. While it's technically possible to do what you ask, it may be bad for usability.
I had the same requirment. Rules worked for me.

Resources