If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribePut on your publishing hat, because it's time to write some thoughtful space articles and insert some rows into our article table! And, good news! This is probably one of the easiest things to do in Doctrine.
Let's create a new controller called ArticleAdminController
. We'll use this as a place to add new articles. Make it extend the normal AbstractController
:
... lines 1 - 2 | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
... lines 6 - 8 | |
class ArticleAdminController extends AbstractController | |
{ | |
... lines 11 - 17 | |
} |
And create a public function new()
:
... lines 1 - 8 | |
class ArticleAdminController extends AbstractController | |
{ | |
... lines 11 - 13 | |
public function new() | |
{ | |
... line 16 | |
} | |
} |
Above, add the @Route()
- make sure to auto-complete the one from Symfony Components
so that PhpStorm adds the use
statement. For the URL, how about /admin/article/new
:
... lines 1 - 6 | |
use Symfony\Component\Routing\Annotation\Route; | |
class ArticleAdminController extends AbstractController | |
{ | |
/** | |
* @Route("/admin/article/new") | |
*/ | |
public function new() | |
{ | |
... line 16 | |
} | |
} |
We're not actually going to build a real page with a form here right now. Instead, I just want to write some code that saves a dummy article to the database.
But first, to make sure I haven't screwed anything up, return a new Response
: the one from HttpFoundation
with a message:
space rocks... include comets, asteroids & meteoroids
... lines 1 - 5 | |
use Symfony\Component\HttpFoundation\Response; | |
... lines 7 - 8 | |
class ArticleAdminController extends AbstractController | |
{ | |
/** | |
* @Route("/admin/article/new") | |
*/ | |
public function new() | |
{ | |
return new Response('space rocks... include comets, asteroids & meteoroids'); | |
} | |
} |
Now, we should be able to find the browser and head to /admin/article/new
. Great!
So, here's the big question: how do you save data to the database with Doctrine? The answer... is beautiful: just create an Article
object with the data you need, then ask Doctrine to put it into the database.
Start with $article = new Article()
:
... lines 1 - 4 | |
use App\Entity\Article; | |
... lines 6 - 9 | |
class ArticleAdminController extends AbstractController | |
{ | |
... lines 12 - 14 | |
public function new() | |
{ | |
$article = new Article(); | |
... lines 18 - 45 | |
} | |
} |
For this article's data, go back to the "Why Asteroids Taste like Bacon" article: we'll use this as our dummy news story. Copy the article's title, then call $article->setTitle()
and paste:
... lines 1 - 9 | |
class ArticleAdminController extends AbstractController | |
{ | |
... lines 12 - 14 | |
public function new() | |
{ | |
$article = new Article(); | |
$article->setTitle('Why Asteroids Taste Like Bacon') | |
... lines 19 - 45 | |
} | |
} |
This is one of the setter methods that was automatically generated into our entity:
... lines 1 - 9 | |
class Article | |
{ | |
... lines 12 - 18 | |
/** | |
* @ORM\Column(type="string", length=255) | |
*/ | |
private $title; | |
... lines 23 - 48 | |
public function setTitle(string $title): self | |
{ | |
$this->title = $title; | |
return $this; | |
} | |
... lines 55 - 91 | |
} |
Oh, and the generator also made all the setter methods return $this
, which means you can chain your calls, like: ->setSlug()
, then copy the last part of the URL, and paste here. Oh, but we need to make sure this is unique... so just add a little random number at the end:
... lines 1 - 9 | |
class ArticleAdminController extends AbstractController | |
{ | |
... lines 12 - 14 | |
public function new() | |
{ | |
$article = new Article(); | |
$article->setTitle('Why Asteroids Taste Like Bacon') | |
->setSlug('why-asteroids-taste-like-bacon-'.rand(100, 999)) | |
... lines 20 - 45 | |
} | |
} |
Then, ->setContent()
. And to get this, go back to ArticleController
, copy all of that meaty markdown and paste here. Ah, make sure the content is completely not indented so the multi-line text works:
... lines 1 - 9 | |
class ArticleAdminController extends AbstractController | |
{ | |
... lines 12 - 14 | |
public function new() | |
{ | |
$article = new Article(); | |
$article->setTitle('Why Asteroids Taste Like Bacon') | |
->setSlug('why-asteroids-taste-like-bacon-'.rand(100, 999)) | |
->setContent(<<<EOF | |
Spicy **jalapeno bacon** ipsum dolor amet veniam shank in dolore. Ham hock nisi landjaeger cow, | |
lorem proident [beef ribs](https://baconipsum.com/) aute enim veniam ut cillum pork chuck picanha. Dolore reprehenderit | |
labore minim pork belly spare ribs cupim short loin in. Elit exercitation eiusmod dolore cow | |
**turkey** shank eu pork belly meatball non cupim. | |
Laboris beef ribs fatback fugiat eiusmod jowl kielbasa alcatra dolore velit ea ball tip. Pariatur | |
laboris sunt venison, et laborum dolore minim non meatball. Shankle eu flank aliqua shoulder, | |
capicola biltong frankfurter boudin cupim officia. Exercitation fugiat consectetur ham. Adipisicing | |
picanha shank et filet mignon pork belly ut ullamco. Irure velit turducken ground round doner incididunt | |
occaecat lorem meatball prosciutto quis strip steak. | |
Meatball adipisicing ribeye bacon strip steak eu. Consectetur ham hock pork hamburger enim strip steak | |
mollit quis officia meatloaf tri-tip swine. Cow ut reprehenderit, buffalo incididunt in filet mignon | |
strip steak pork belly aliquip capicola officia. Labore deserunt esse chicken lorem shoulder tail consectetur | |
cow est ribeye adipisicing. Pig hamburger pork belly enim. Do porchetta minim capicola irure pancetta chuck | |
fugiat. | |
EOF | |
); | |
... lines 39 - 45 | |
} | |
} |
Much better! The last field is publishedAt
. To have more interesting data, let's only publish some articles. So, if a random number between 1 to 10 is greater than 2, publish the article: $article->setPublishedAt()
with new \DateTime()
and sprintf('-%d days')
with a bit more randomness: 1 to 100 days old:
... lines 1 - 9 | |
class ArticleAdminController extends AbstractController | |
{ | |
... lines 12 - 14 | |
public function new() | |
{ | |
$article = new Article(); | |
$article->setTitle('Why Asteroids Taste Like Bacon') | |
->setSlug('why-asteroids-taste-like-bacon-'.rand(100, 999)) | |
->setContent(<<<EOF | |
Spicy **jalapeno bacon** ipsum dolor amet veniam shank in dolore. Ham hock nisi landjaeger cow, | |
lorem proident [beef ribs](https://baconipsum.com/) aute enim veniam ut cillum pork chuck picanha. Dolore reprehenderit | |
labore minim pork belly spare ribs cupim short loin in. Elit exercitation eiusmod dolore cow | |
**turkey** shank eu pork belly meatball non cupim. | |
Laboris beef ribs fatback fugiat eiusmod jowl kielbasa alcatra dolore velit ea ball tip. Pariatur | |
laboris sunt venison, et laborum dolore minim non meatball. Shankle eu flank aliqua shoulder, | |
capicola biltong frankfurter boudin cupim officia. Exercitation fugiat consectetur ham. Adipisicing | |
picanha shank et filet mignon pork belly ut ullamco. Irure velit turducken ground round doner incididunt | |
occaecat lorem meatball prosciutto quis strip steak. | |
Meatball adipisicing ribeye bacon strip steak eu. Consectetur ham hock pork hamburger enim strip steak | |
mollit quis officia meatloaf tri-tip swine. Cow ut reprehenderit, buffalo incididunt in filet mignon | |
strip steak pork belly aliquip capicola officia. Labore deserunt esse chicken lorem shoulder tail consectetur | |
cow est ribeye adipisicing. Pig hamburger pork belly enim. Do porchetta minim capicola irure pancetta chuck | |
fugiat. | |
EOF | |
); | |
// publish most articles | |
if (rand(1, 10) > 2) { | |
$article->setPublishedAt(new \DateTime(sprintf('-%d days', rand(1, 100)))); | |
} | |
return new Response('space rocks... include comets, asteroids & meteoroids'); | |
} | |
} |
Perfect! Now... stop. I want you to notice that all we've done is create an Article
object and set data on it. This is normal, boring, PHP code: we're not using Doctrine at all yet. That's really cool.
To save this, we just need to find Doctrine and say:
Hey Doctrine! Say hi to Jon Wage for us! Also, can you please save this article to the database. You're the best!
How do we do this? In the last Symfony tutorial, we talked about how the main thing that a bundle gives us is more services. DoctrineBundle gives us one, very important service that's used for both saving to and fetching from the database. It's called the DeathStar. No, no, it's the EntityManager. But, missed opportunity...
Find your terminal and run:
php bin/console debug:autowiring
Scroll to the the top. There it is! EntityManagerInterface
: that's the type-hint we can use to fetch the service. Go back to the top of the new()
method and add an argument: EntityManagerInterface $em
:
... lines 1 - 5 | |
use Doctrine\ORM\EntityManagerInterface; | |
... lines 7 - 10 | |
class ArticleAdminController extends AbstractController | |
{ | |
... lines 13 - 15 | |
public function new(EntityManagerInterface $em) | |
{ | |
... lines 18 - 53 | |
} | |
} |
Now that we have the all-important entity manager, saving is a two-step process... and it may look a bit weird initially. First, $em->persist($article)
, then $em->flush()
:
... lines 1 - 5 | |
use Doctrine\ORM\EntityManagerInterface; | |
... lines 7 - 10 | |
class ArticleAdminController extends AbstractController | |
{ | |
... lines 13 - 15 | |
public function new(EntityManagerInterface $em) | |
{ | |
... lines 18 - 40 | |
// publish most articles | |
if (rand(1, 10) > 2) { | |
$article->setPublishedAt(new \DateTime(sprintf('-%d days', rand(1, 100)))); | |
} | |
$em->persist($article); | |
$em->flush(); | |
... lines 48 - 53 | |
} | |
} |
It's always these two lines. Persist simply says that you would like to save this article, but Doctrine does not make the INSERT query yet. That happens when you call $em->flush()
. Why two separate steps? Well, it gives you a bit more flexibility: you could create ten Article objects, called persist()
on each, then flush()
just one time at the end. This helps Doctrine optimize saving those ten articles.
At the bottom, let's make our message a bit more helpful, though, I thought my message about space rocks was at least educational. Set the article id to some number and the slug to some string. Pass: $article->getId()
and $article->getSlug()
:
... lines 1 - 5 | |
use Doctrine\ORM\EntityManagerInterface; | |
... lines 7 - 10 | |
class ArticleAdminController extends AbstractController | |
{ | |
... lines 13 - 15 | |
public function new(EntityManagerInterface $em) | |
{ | |
... lines 18 - 45 | |
$em->persist($article); | |
$em->flush(); | |
return new Response(sprintf( | |
'Hiya! New Article id: #%d slug: %s', | |
$article->getId(), | |
$article->getSlug() | |
)); | |
} | |
} |
Oh, and this is important: we never set the id. But when we call flush()
, Doctrine will insert the new row, get the new id, and put that onto the Article
for us. By the time we print this message, the Article will have its new, fancy id.
Ok, are you ready? Let's try it: go back to /admin/article/new
and... ha! Article id 1, then 2, 3, 4, 5, 6! Our news site is alive!
If you want to be more sure, you can check this in your favorite database tool like phpMyAdmin or whatever the cool kids are using these days. Or, you can use a helpful console command:
php bin/console doctrine:query:sql "SELECT * FROM article"
This is article
with a lowercase "a", because, thanks to the default configuration, Doctrine creates snake case table and column names.
And... yes! There are the new, 6 results.
We have successfully put stuff into the database! Now it's time to run some queries to fetch it back out.
Hey Skylar!
Ha! You just made my morning! I feel properly showered in praise - thanks ;).
So..... great question - probably there aren't a lot of answers... because it's complicated... so nobody knows what to say :). First, do you need to track timezones / handle timezones? What I mean is, do you have situations where a user enters a time in their local timezone, and you need to store it in the database? Or, are your dates more internal - e.g. createdAt dates, etc?
We only handle this in a few places in KnpU, but our solution is always to store dates in UTC. In the few places where our users need to enter a time (actually, this only happens in our admin), we make sure to convert the date from the user's timezone into a UTC DateTime before setting it on our entity. Ultimately, everything in the database is in UTC. IF we needed to re-render a date in a local timezone later (we don't have this requirement, but, if we did), we would just need to (A) know which timezone to convert to (which is why you see the timezone sometimes stored in the database - so you know how to convert it *back* later) and then (B) use that to convert the date when rendering it (but keep it in UTC on your entity). Heck, if you had an entity like Event that had a startDateTime field and a timezone field, you could add a new getStartDateInLocalTimezone() method that would create a new DateTime based on the startDateTime UTC and the timezone.
Does that help? Or is there still a missing best-practice piece?
Cheers!
Storing all dates in the database in UTC was my conclusion too. So..... How can we modify the TimeStampableEntity Trait to do that? Or should we implement the conversion as described in the links?
Hey Skylar
I believe the "TimeStampableEntity" will grab your server's timezone by default, so if you want to always make sure that the date is in UTC, you could do it via a listener, or by creating your own TimeStampableEntity which internally will make that conversion.
I've been using Symfony and Doctrine for a few months now, but there's always been a couple of things about Doctrine that made me wonder.
In this example you have added the argument EntityManagerInterface $em to the new() method. Could you tell me why you choose the EntityManagerInterface over the EntityManager?
I usually use $em = $this->getDoctrine()->getManager();
somewhere in the method where i need it. This works, but my IDE tells me that in this case, $em is actually the ObjectManager. Can you explain what the differences are between these three classes (EntityManagerInterface, EntityManager and ObjectManager) and when to use which?
Hey Dirk
Great question!
The ObjectManager interface allows you to work with ORM and ODM, so if for some reason you are using both, or you are planning to switch one to another, then you should type hint your dependencies with "ObjectManager"
The "EntityManagerInterface" contain some specific methods for working with a ORM, like "createQueryBuilder"
and "EntityManager" is just the implementation of "EntityManagerInterface"
No matter what, whenever you can, you should use interfaces for type-hinting your dependencies
Cheers!
Hi Diego,
Thanks for taking the time to respond. I'm still somewhat confused between the ObjectManager (which seems to actually be the interface?) and the EntityManagerInterface. From an example on the Symofony website:
`
// you can fetch the EntityManager via $this->getDoctrine()
// or you can add an argument to your action: index(EntityManagerInterface $entityManager)
$entityManager = $this->getDoctrine()->getManager();
`
When I use this method, $entityManager is actually the ObjectManager according to my IDE. When I use: public function new(EntityManagerInterface $entityManager)
then $entityManager is the EntityManagerInterface according to my IDE. Is that correct? And if so, does it not really matter in most cases if you're only going to use the ORM? Sorry I ask this again, but it is really confusing to me.
Yes, you are correct , this line $this->getDoctrine()->getManager();
returns you an instance of ObjectManager
interface, which is the main abstraction for woking with ORM and ODM, but as I said, if you are going full ORM, then you don't have to worry about it and type-hint your dependencies with EntityManagerInterface
.
Oh, and if you are on Symfony3.3 or higher, then you should be using dependency injection all the time, in other words, do not use $this->getDoctrine()
any more :)
Cheers!
Hi,
lets say I have a table for storing appointments. A user wants to save 4 appointments, same time and day of the week for the next four weeks.
When I get the first appointment from the form, I can save it without any problem. But,of course, when I change the date of this appointment and flush()
it to the database, it changes the date of my first appointment.
So I thought I could make an $appointment2 = new Appointment()
; Copy my first appointment on this new one ($appointment2 = $appointment1
), change the date on the copy and flush()
the copy. But, to my surprise, again my first appointment is changed.
What is the correct way to do this?
Thx
Oliver
Hey @Oliver-W ,
Keep in mind that the date field is an object :) So, you want to "clone" that object before setting it to the 2nd appointment, otherwise you will set the same object to 2 different entities. And of course, changing the date in one appointment will automatically change the date in the second appointment because that's the same object :)
Basically, just clone the date with $appointment2Date = clone $appointment1Date
where the $appointment1Date
is a DateTime
object from the form. It should do the trick :)
Also, I would recommend you to use datetime_immutable
instead of simple datetime
for all your dates - it's more "hipster" now :) You can read about the difference in the docs: https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/types.html#datetime-immutable
Cheers!
Hi, I just started the symfony 4 tutorial doctrine and the database. Creating ArticleAdminController.php and getting the first message in the browser to check the route worked. After finishing that controller with persist and flush I got the error "Attempted to load class "Article" from namespace "App\Controller".
Did you forget a "use" statement for another namespace?" I double checked for typos, but couldnt find any. Can anybody give me a hint about the error message.
Hey Meike H.
You just need to add the use
statement for you Article entity. In other words, add this at the top of your controller (after the namespace)
// App\Controller\YourController.php
use App\Entity\Article;
...
Cheers!
Hey Diego,
thanks a lot! It worked. I retyped that section and phpstorm inserted the use-statement automatically. Don't know what went wrong the first time. Maybe I typed the function and didn't use the autocomplete hint.
And thanks to ryan and the symfonycast team for the great tutorials!
Unlikely that anyone is reading this, but if you are: There's no need to un-indent your Heredoc comments since PHP 7.3! As long as your start and end DOC markers are lined up it should all work fine.
https://laravel-news.com/fl...
And that includes using them within an array argument. How cool is that?
Oh, but we need to make sure this is unique... so just add a little random number at the end:
Bad luck. After 7 attempts --> Integrity constraint violation: 1062 Duplicate entry 'why-asteroids-taste-like-bacon311' for key 'UNIQ_23A0E66989D9B62'
Thx for the tutorials, I love them :)
Hello, thanks a lot for these great videos. I just have a slight suggestion, and that is perhaps at the end of each video or at the bottom of the script, can all the includes at the top of the page, which includes the namespace declarations and the use declarations be included?
For those of us who aren't using phpstorm, it is quite difficult to keep up with adding those into the file by hand if they aren't shown in the video. Taking the file itself from the finish directory of the included code doesn't always work because sometimes the file is completely different than how it is at the end of an episode.
Most of my mistakes have came from not included something and being confused why I'm getting an error but eventually I figure it out.
Thank you!
Hey Galen S.!
Thanks for the feedback! I try to be aware of users not using PhpStorm... but even still... yea, we rely on it to do a lot of us (and, even if you're aware of what it's doing, you still can't see the use statements!). I *may* have one suggestion for you - if you haven't noticed it already: all of the code for each chapter is included below each video in code blocks. We only show the "important" parts by default (which should at least *usually* include the use statements, etc) but you can click a button to "expand" and see the whole file. You'd probably still need, for example, 2 tabs open - so you can switch from the video to the code blocks, but I hope it will help. It's one of our solutions for this exact problem.
Let me know!
Cheers!
Oh my! You guys are great, and are already on it! I love these videos and this site. I hadn't noticed the "show all lines" button in the top right before, this is exactly what I needed, thank you!
Interestingly, you can specify the table name with any case in the command:
php bin/console doctrine: query: sql " SELECT * FROM ARTICLE"
php bin/console doctrine: query: sql " SELECT * FROM article"
php bin/console doctrine: query: sql 'SELECT * FROM "ARTIclE"'
And everything works perfectly!
I use PostgreSQL, that's how It behaves if I make queries not through Doctrine, but directly:
https://stackoverflow.com/q...
Probably Doctrine converts the query to lowercase?
Hey Maxim M.
That's interesting, I believe the `QueryBuilder` for PostgreSQL may have some logic for converting your query string into a proper PostgreSQL query
Cheers!
Hello, I'm facing this problems,
Attempted to load interface "NotExistingInterface" from namespace
"Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation".
Did you forget a "use" statement for another namespace?
Symfony\Component\Debug\Exception\
ClassNotFoundException
in vendor\symfony\framework-bundle\Tests\Fixtures\Validation\Article.php (line 5)
Hey Rashid,
Hm, I only can think that you trying to use this "Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation\Article" class somewhere in your code, see this reference: https://github.com/symfony/...
I suppose you autocompleted a wrong namespace somewhere, try to find the place where you use that namespace. I think you need to replace it with "App\Entity\Article" one. Let me know if you still have this error.
Cheers!
Got an error when i hit /admin/article/new at the end of the tutorial:
Type error: Argument 1 passed to App\Entity\Article::setPublishedAt() must be an instance of DateTimeImmutable or null, instance of DateTime given, called in /var/www/phptut/src/Controller/ArticleAdminController.php on line 46
Fixxed it by changing \DateTime to DateTimeImmutable in Line 46: $article->setPublishedAt(new \DateTimeImmutable(sprintf('-%d days', rand(1, 100))));
can u explain what happened here? Cheers
Could it be that you have the "type" of "publishedAt" in your Article class set to "datetime_immutable"? So in article controller you see something like:
` /**
* @ORM\Column(type="datetime_immutable", nullable=true)
*/
private $publishedAt;`
If so, that created the error, because you are setting this attribute to 'datetime_immutable' instead of 'datetime'.
Perfect! Thanks.
i wonder why it set it to datetime_immutable. Must have been some weird autocomplete when i created the entity.
No problem. Yes, i'm pretty sure it happened with autocompletion, because datetime_immutable is actually a type you could use.
Just a side note: Windows users should use double quotes on sql command: "SELECT * FROM article"
Hey Daniel,
Hm, interesting, thanks for sharing it with others. But why not single quotes? Do you see some errors?
Cheers!
On Windows 10 in the standard cmd and in the terminal PhpStorm (which probably works on the basis of cmd):
php bin/console doctrine:query:sql 'SELECT * FROM ARTICLE'
Too many arguments, expected arguments "command" "sql".
In PowerShell or MINGW64 works with any quotes.
Hey Maxim,
Thanks for the detailed explanation! I actually didn't know that quotes are so important on Windows. Windows console sucks and have weird behavior sometimes :/
Btw, I heard good feedbacks about this console emulator for Windows: https://github.com/cmderdev... . Just in case, maybe it might be useful for other users.
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knplabs/knp-time-bundle": "^1.8", // 1.8.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.1.4
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.0.4
"symfony/console": "^4.0", // v4.0.14
"symfony/flex": "^1.0", // v1.17.6
"symfony/framework-bundle": "^4.0", // v4.0.14
"symfony/lts": "^4@dev", // dev-master
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/twig-bundle": "^4.0", // v4.0.4
"symfony/web-server-bundle": "^4.0", // v4.0.4
"symfony/yaml": "^4.0" // v4.0.14
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.4
"fzaninotto/faker": "^1.7", // v1.7.1
"symfony/debug-bundle": "^3.3|^4.0", // v4.0.4
"symfony/dotenv": "^4.0", // v4.0.14
"symfony/maker-bundle": "^1.0", // v1.4.0
"symfony/monolog-bundle": "^3.0", // v3.1.2
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.4
"symfony/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.0.4
}
}
Once again, you have made Symfony awesome for me, so let me shower some praise. ::shower::
I don't know if this is the right place for this question, but it is regarding Doctrine/MySQL and TimeZones. I was thinking that it would be great for all the DateTime's in MySQL to be in UTC, so that east/west coast programming would not have to deal with EST/EDT. So I googled for an answer for some kind of best practice and did not find a consensus.
I have relied on your Best Practice's and they have made my programming life so much easier (even when you make it more complicated :) ).
So I found these to articles:
https://www.doctrine-projec...
https://blog.bestcoding.net...
What are your thoughts on the subject and is this the best way to implement it. Or can it be "KNPUniversity'ed" up?