Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Creating a Service Class

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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:

... 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:

... 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():

... 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():

... 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:

... 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:

... 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!

30
Login or Register to join the conversation

I know it gets old but there 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.

1 Reply
Default user avatar
Default user avatar Juan Nicolás | posted 5 years ago

A service should have just one function? Or can I create a "TextStyle service" and inside functions like: normal, italic, bold, etc? (just and example). Thanks.

Reply

Hey Juan,

No, service could have more than one function, it's just a simple PHP class, so feel free to put there as much methods as you need till it's semantically ok. But if some methods weakly related to the service, create another one for them. However, the more methods your service has - the harder to use it.

Cheers!

Reply
Default user avatar
Default user avatar Juan Nicolás | Victor | posted 5 years ago

4 am here at my country, I'm working, tired but learning with KNP. Victor, thanks for your answer, you can't imagine the importance of answers like this for me.

Thanks again :)

Reply

Hey Juan,

Yea, I know, having a good advise is always great ;) Where're you from btw?

Cheers!

Reply
Default user avatar
Default user avatar Juan Nicolás | Victor | posted 5 years ago

I'm from Colombia and I don't speak English, but I can understand jokes between the videos.

Cheers!

Reply

Hey Juan,

Haha, understanding jokes is half the work already ;) Then, I bet you love our course scripts below the video and if you use Google Chrome browser - you can easily translate translate it on the go.

And hello to Colombia!

Reply
Default user avatar
Default user avatar Juan Nicolás | Victor | posted 5 years ago

Of course Victor!

Even I downloaded the PDF to read later (like 1000 times...). I using this to practice my English with the intention of travel to your country and say "thanks" with a good pronunciation.

Talking again about services, I have another question if you can help me, please: Is okay use inheritance between services classes or better use Dependency Injection HA, HA, HA? (I need too parent's variables - $name, $lastname, $example).

Thanks one more time.

Reply

Hey Juan,

Haha, that's really cool when you study programming and English at once :)

Hm, it depends! Actually, both inheritance and Dependency Injection (DI) are good.And probably it's a bit incorrect question, because Inheritance and DI are different things. You probably still need to use DI if you use inheritance. But anyway, if you have a few services which do the same task but with different implementation - it's good to use inheritance. But if your services are completely different - their inheritance probably will mislead you and other developers. So start with DI, and if you see something common in those services, create an abstract class and inherit it by those services. Then you can move some common fields to the abstract class to avoid duplication, but... you still need to use DI for those services to inject what you need, even if it's moved to the abstract class. Am I understand you right? Does it make sense for you?

Cheers!

1 Reply
Default user avatar
Default user avatar Juan Nicolás | Victor | posted 5 years ago

I'll answer you with a translator, excuse the inconsistencies.

Thanks to your answers I think I identified my confusion: I do not know the difference of an Entity, a Service and a Model; Consequently I do not know how to write clean code in Symfony.

Following your videos (KNP) and this answer ( https://stackoverflow.com/a... ) I came to the following conclusions:

- Symfony is not entirely an MVC pattern.
- Entity: Persistence layer with database (that's new for me).
- Model: Can be created and contains business logic (interacts with the database by injecting an Entity object).
- Service: Works as a container that unifies an entity and a model that is called in the controllers using the symfony container.

That is roughly what I have in my head and obviously I do not quite understand how to apply it in the real world. Let me briefly explain my case, Victor.

MVC project without Symfony (and no frameworks, just jQuery): I created a database (mySQL) with a table called Pokémon, a model called Pokémon whose attributes are called equal to the table fields (name, attack, defense, Speed), in addition, within the Model there are some getters who consult the information of the Pokémon in the database.

Then, I created another Model called PokémonGO that inherits Pokémon to use its attributes and data to perform more complex operations with each Pokémon, for example: simulateBatalla (), comparePokemon (), saveFavorite (); Things like that, tools that help fans.

All operations use AJAX with jQuery to print results on the screen or save a favorite Pokémon in the database (in another SQL table, of course).

Knowing all this and trying to do it with Symfony from scratch, I'm doing the following:

- The table called Pokémon is created by an Entity.
- The Model called Pokémon try to see it as a Service and create another Service called PokémonGO that inherits Pokémon. In this part I believe that I can inject a Pokémon object into PokémonGO, although honestly I say it is for trying to understand.

Sorry to ask for your help in this way, but really, I tried adapting the course examples but as you see I am confused. (Symfony used Confusion - it is very effective). I do not ask you the solution to my problem although knowing a logical structure of folders or order of the code would be enough for me.

I am a developer who has always avoided the frameworks until I believe I understand what is behind them, now I just try to adapt to one of the most powerful frameworks.

Victor and the KNP team, thanks for the patience and biting. :)

(Oh, inclusive, I would like to know if there is any video explaining how to deploy a Symfony project on the server, internet. Is it enough to copy the project folder on the server ?, thanks).

Reply

Hey Juan,

Wow, that was a huge comment :) You know, sometimes you just needed more practice. You'd probably refactor a lot of your own code over time at the initial stage. So it's impossible to come up with ideal solution. The code that you thought is clean will seem bad to you after a while.

Yes, Symfony is not an MVC but more Request/Response framework. Your application should handle a request and return a response.

What about your project, the important part is an Entity, so you need to start with it first. Entity is just a simple PHP object which holds data. But around this data you'll build your application. So you need to create a Pokemon entity, which will have properties (which holds data) and setters/getters to set and get those properties, and that's it. No any other logic should be in the entity.

To apply complex queries to those entities you need a repository, so you can create PokemonRepository where you will hold queries like findAllMyPokemons(), findTheMostPopularPokemon(), findRandomPokemonForBattle(), etc.

Then, to control entities - you'll need a controller. So you can create PokemonController which will have createAction(), simulateBatallaAction(), comparePokemonAction(), saveFavoriteAction(), etc. You will send GET/POST or AJAX requests to this endpoints and *do* something with your pokemons (your entities) accordingly, executing complex queries from repository, etc.

And only then, when you need to reuse some common logic between controllers, you can create a service, move this common logic there, and call it in different controllers where you need it.

That's it, that was the order you can start coding your pokemon application. I hope it was helpful for you.

What about Symfony deploy, we will have a new course about it soon: https://knpuniversity.com/s... , but this course based on knowledges of Ansible, so you'd probably want to see this course first: https://knpuniversity.com/s... . But Ansistrano is just a tool as many others, so fell free to choose another one. I think this official docs will be useful for you: https://symfony.com/doc/cur... . If you have any question after reading it - just ask ;)

Cheers!

Reply
Default user avatar
Default user avatar Juan Nicolás | Victor | posted 5 years ago

I appreciate your time!
I will put in practice all your help. Victor, thanks you very much.

Reply
Default user avatar

Hi Ryan,
I have a term that confused me a little that I read on some sites, maybe you can help me?
- Modular Programing
- You should write Modular code...

So I understand that modular programming are the same like mentioned this course:

"move a chunk of code out" in a function or class, is this right?

Is than this service-oriented arhitecture, modular programming.
(Sorry about my english :)

Reply

Hi Denis!

Basically, I'd say yes :). Both ideas/terms are definitely referring to the same idea: moving chunks of code into their own classes / functions. If you do this, then instead of having controllers with TONS of code, you have many, small "modules" (i.e. classes) of functionality that you can re-use anywhere. So yes, you're thinking correctly!

Cheers!

Reply
Default user avatar
Default user avatar Konrad Zając | posted 5 years ago

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???

Reply

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/screencast/symfony/first-page

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!

Reply
Default user avatar
Default user avatar Konrad Zając | weaverryan | posted 5 years ago

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

Reply

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!

Reply
Default user avatar
Default user avatar Konrad Zając | weaverryan | posted 5 years ago

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

Reply

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!

Reply
Default user avatar

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

Reply
Default user avatar
Default user avatar Terry Caliendo | posted 5 years ago | edited

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'));<br />die();

Reply

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!

Reply
Default user avatar
Default user avatar Terry Caliendo | weaverryan | posted 5 years ago

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/dis...

Thanks again!

Reply
Default user avatar
Default user avatar Patrick Van Krugten | posted 5 years ago

Hi Ryan,

You have a nice tutorial about the Symfony! But i'm stuck on this chapter.
I use PHPstorm and i created the new folder in AppBundle named 'Service'.
I also created the file 'MarkdownTransformer' with the right code.

Now if i go back to the controller and try to typ '$markdownParser = new $MarkdownTransformer' there is one auto complication but not the one in the Service map... I have tried to fix it manually and add in the top 'use AppBundle\Service\MarkdownTransformer' but then the Service part become red. It looks like he don't recognised the folder.

Can you please help my!

Thank you

Reply

Hey Patrick Van Krugten!

Thanks man - glad you're enjoying the tutorial! :)

Ok, about your issue: open your MarkdownTransformer class and make sure the namespace at the top is set correctly (should be AppBundle\Service). The clue is that PhpStorm is auto-completing your class, but is not auto-completing with the correct use statement. When PhpStorm autocompletes, it simply reads the namespace from that target class (e.g. MarkdownTransformer) and uses that for the use statement. If the namespace is wrong or missing, you'll get the wrong auto-complete! You did a good job at recognizing that this incorrect auto-complete was a sign of a problem!

Let me know if that fixes the issue!

Cheers!

Reply
Default user avatar
Default user avatar Patrick Van Krugten | weaverryan | posted 5 years ago | edited

Thx weaverryan

The problem was i had not a namespace.
i had created a new project and forgot to set the source root in PHPStorm

Thank you again!

---

Can i also send you a another question? but it's not about this course but about building a small application with symfony to practice. I'm already started, but i'm not nothing what the do next and how to.

Reply
Default user avatar

I have recently downloaded the course files. While using the start folder, every time I try to access localhost:8000/genus I get a 404 error displaying "No route found for "GET /genus/" ". I can however, access the homepage and localhost:8000/genus/new. Is there an issue with the files or is it an issue on my end? Perhaps I missed a step?

Reply
Default user avatar

Worked it out. Above list action, the *@Route("/genus") needs a forward slash like so: *@Route("/genus/")

Reply

Hey Stc

As you just figured it out, Symfony routing system has that defect when working with paths that does not end with a slash, but that's about to change in Symfony 4.1. Check this out: https://symfony.com/blog/ne...

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice