Buy

Ready to move a chunk of code out of the controller? Well good for you.

Step 1: create a new PHP class. In AppBundle, I'll create a new directory called Service - but that could be called anything. Inside, add a new PHP class called MarkdownTransformer:

12 lines src/AppBundle/Service/MarkdownTransformer.php
... lines 1 - 2
namespace AppBundle\Service;
class MarkdownTransformer
{
... lines 7 - 10
}

If you're keeping score at home, that could also be called anything.

Start this with one public function parse() with a $str argument:

12 lines src/AppBundle/Service/MarkdownTransformer.php
... lines 1 - 4
class MarkdownTransformer
{
public function parse($str)
{
return strtoupper($str);
}
}

Eventually, this will do all the dirty work of markdown parsing and caching. But for now... keep it simple and return strtoupper($str). But use your imagination - pretend like it totally is awesome and is parsing our markdown. In fact, it's so awesome that we want to use it in our controller. How?

Find GenusController. First, create a new object with $transformer = new MarkdownTransformer():

125 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 58
public function showAction($genusName)
{
$em = $this->getDoctrine()->getManager();
$genus = $em->getRepository('AppBundle:Genus')
->findOneBy(['name' => $genusName]);
if (!$genus) {
throw $this->createNotFoundException('genus not found');
}
$markdownParser = new MarkdownTransformer();
... lines 71 - 97
}
... lines 99 - 123
}

Noooothing special here: the new method is purposefully not static, and this means we need to instantiate the object first. Next, add $funFact = $transformer->parse() and pass $genus->getFunFact():

125 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 69
$markdownParser = new MarkdownTransformer();
$funFact = $markdownParser->parse($genus->getFunFact());
... lines 72 - 125

And that's it! If you're feeling massively underwhelmed... you're right where I want you! I want this to be boring and easy - there are fireworks and exciting stuff later.

Finish this by passing $funFact into the template so we can render the parsed version:

125 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 58
public function showAction($genusName)
{
... lines 61 - 69
$markdownParser = new MarkdownTransformer();
$funFact = $markdownParser->parse($genus->getFunFact());
... lines 72 - 92
return $this->render('genus/show.html.twig', array(
... line 94
'funFact' => $funFact,
... line 96
));
}
... lines 99 - 123
}

Then, open the template and replace genus.funFact with just funFact:

42 lines app/Resources/views/genus/show.html.twig
... lines 1 - 4
{% block body %}
<h2 class="genus-name">{{ genus.name }}</h2>
<div class="sea-creature-container">
<div class="genus-photo"></div>
<div class="genus-details">
<dl class="genus-details-list">
... lines 12 - 15
<dt>Fun Fact:</dt>
<dd>{{ funFact }}</dd>
... lines 18 - 19
</dl>
</div>
</div>
<div id="js-notes-wrapper"></div>
{% endblock %}
... lines 25 - 42

Try it out: open up localhost:8000/genus - then click one of the genuses. Yes! The fun fact is screaming at us in upper case.

So believe it or not: you just saw one of the most important and commonly-confusing object-oriented strategies that exist anywhere... in any language! And it's this: you should take chunks of code that do things and move them into an outside function in an outside class. That's it.

Oh, and guess what? MarkdownTransformer is a service. Because remember, a service is just a class that does work for us. And when you isolate a lot of your code into these service classes, you start to build what's called a "service-oriented architecture". OooOOoooOOOo. That basically means that instead of having all of your code in big controllers, you organize them into nice little services that each do one job.

Of course, the MarkdownTransformer service isn't actually transforming... any... markdown - so let's fix that.

Leave a comment!

  • 2016-11-07 Terry Caliendo

    Thanks for the detailed answer... it will help me be a better coder. I ended up solving the issue without the redirect. The issue had to do with cloning an entity and the cache-type issue I was having which you helped me solve in another thread.

    My cloning process was a service that returned the Id of the cloned entity. That cloned entity had relations to other entities that were cloned as well. In the calling controller I was querying for that top cloned entity by the returned ID, but I was getting strange results on the related entities due to Doctrines efficiency management. So to solve the cache-type issue, I was creating the entity then redirecting to another page (but the redirect needed to be done from the service) to finish things up. The redirect would create a new session on the parent entity, clearing the cache-type issue.

    So the resolution was to do a more detailed cloning.

    Details in this discussion for anyone that comes across the same issue:
    https://disqus.com/home/discus...

    Thanks again!

  • 2016-11-04 weaverryan

    Hey Terry!

    The short answer is no. And generally speaking, if you need this, you *might* have a bit of a design problem (but maybe not). There are two reasons why you might want to redirect:

    1) Something "normal" happens (e.g. form save) and you want to send the user somewhere. This should be done in the controller. Typically, a service shouldn't know or care what route/controller is being processed or where the user should be sent next: the controller would call the service to do some work, but ultimately direct traffic itself.

    2) Something "exceptional" happens (e.g. in a service, somehow, you find yourself in a situation that shouldn't happen - e.g. perhaps you're processing a new order, but realize that [somehow] the user isn't logged in!). In this case, you should throw an exception from your service. And usually... that's it - I make sure that my errors are logged somewhere so I can address this bug. If, for some reason, this "exceptional" situation isn't *so* exceptional, and you want to "handle" the situation, then you can add a try-catch block for the specific exception in your controller, and then redirect from there.

    That's a long way of asking: can you tell me more about your specific situation? There *is* a good solution... but you're right that calling header();die(); isn't it :). This prevents Symfony from running different shutdown tasks that your app likely depends on.

    So, let me know a bit more! Cheers!

  • 2016-11-04 Terry Caliendo

    Is there an easy way to redirect to another page from within a service without having to return back all the way to the controller?

    My controller calls a member function on a Service. Within that Service member function, I call another member function. Within that sub-function I do a check that needs to redirect to another url.

    I imported the Router, but in order to use the statement below, I would need to bubble the response all the way back to the controller, correct?
    return $this->router->generate('questionnaire');

    Is there a way to just end the script (gracefully) and redirect?
    Is it bad form in Symfony output a header and die. Something like:
    header( 'Location: ' . $this->router->generate('questionnaire'));
    die();

  • 2016-08-25 Chmlls

    Day9? ;D

  • 2016-06-15 JLChafardet

    I know it gets old but weaverryan you sure are good at teaching and keeping ppl engaged in your lectures! kudos to you!
    for real you are the first caster{-that-teaches-something} that I've ever seen (outside a really few good starcraft2 casters) that really keeps me interested in what he/she is saying.

  • 2016-06-09 weaverryan

    Hi Konrad!

    Hmm, so your database was repopulated, but the page is still empty? Exactly which URL are you going to and what are you seeing? Did you download the code from this page? And if so, are you using the start or finish directory?

    Let me know - I'm sure we can figure out the issue :)

    Cheers!

  • 2016-06-06 Konrad Zając

    Hi I did the command it repopulated my databese, but it's still empty.In which project there's somthing done to the database?

  • 2016-05-31 weaverryan

    Hey Konrad!

    Ah yes, also run:


    php bin/console doctrine:fixtures:load

    This will populate your database. Btw, if you download the course code, we of the full setup details in the README.md file that's in both the start/ and finish/ directories. If you have any issues, let me know :).

    Cheers!

  • 2016-05-25 Konrad Zając

    I do see a page after runing that command( previously just an error) but still, no content. Should i add something?

  • 2016-05-25 weaverryan

    Hi Konrad!

    When you create a new class in PhpStorm, it *can* automatically add the "namespace" line for you (which is what you see in this tutorial). But, to get that, you need to mark your "src" directory as a "Sources Root" in PhpStorm. We show that at about 1:30 in this tutorial: http://knpuniversity.com/scree...

    About your error, you *do* have the database, but you don't yet have the database *tables* :). You can get them by running:


    php bin/console doctrine:migrations:migrate

    Let me know if that works! Cheers!

  • 2016-05-24 Luma

    Amazing tutorial! Thanks for your tips. It is helping me understand the fundamentals of symfony.

  • 2016-05-23 Konrad Zając

    HI,
    should the namespace generate automaticly?
    After all the steps I can display the localhost:8000
    but when I visit localhost:8000/genus ther's an error "SQLSTATE[42S02]: Base table or view not found: 1146 Table 'symfony.genus' doesn't exist"
    I did the db connection all right, so where's the problem???