gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
It's you again! Welcome back friend! In this tutorial, we're diving back into Doctrine: this time to master database relations. And to have fun of course - databases are super fun.
Like usual, you should code along with me or risk 7 years of bad luck. Sorry. To do that, download the code from the course page and use the start
directory. I already have the code - so I'll startup our fancy, built-in web server:
./bin/console server:run
You may also need to run composer install
and a few other tasks. Check the README in the download for those details.
When you're ready, pull up the genus list page at http://localhost:8000/genus
. Nice!
Click to view a specific genus. See these notes down here? These are loaded via a ReactJS app that talks to our app like an API. But, the notes themselves are still hardcoded right in a controller. Bummer! Time to make them dynamic - time to create a second database table to store genus notes.
To do that, create a new GenusNote
class in the Entity
directory. Copy the ORM use
statement from Genus
that all entities need and paste it here:
... lines 1 - 2 | |
namespace AppBundle\Entity; | |
use Doctrine\ORM\Mapping as ORM; | |
... lines 6 - 10 | |
class GenusNote | |
{ | |
... lines 13 - 78 | |
} |
With that, open the "Code"->"Generate" menu - or Cmd
+ N
on a Mac - and select "ORM Class":
... lines 1 - 2 | |
namespace AppBundle\Entity; | |
use Doctrine\ORM\Mapping as ORM; | |
/** | |
* @ORM\Entity | |
* @ORM\Table(name="genus_note") | |
*/ | |
class GenusNote | |
{ | |
... lines 13 - 78 | |
} |
Bam! This is now an entity!
Next: add the properties we need. Let's see... we need a username
, an userAvatarFilename
, notes
and a createdAt
property:
... lines 1 - 10 | |
class GenusNote | |
{ | |
... lines 13 - 17 | |
private $id; | |
... lines 19 - 22 | |
private $username; | |
... lines 24 - 27 | |
private $userAvatarFilename; | |
... lines 29 - 32 | |
private $note; | |
... lines 34 - 37 | |
private $createdAt; | |
... lines 39 - 78 | |
} |
When we add a user table later - we'll replace username
with a relationship to that table. But for now, keep it simple.
Open the "Code"->"Generate" menu again and select "ORM Annotation". Make sure each field type
looks right. Hmm, we probably want to change $note
to be a text
type - that type can hold a lot more than the normal 255 characters:
... lines 1 - 10 | |
class GenusNote | |
{ | |
/** | |
* @ORM\Id | |
* @ORM\GeneratedValue(strategy="AUTO") | |
* @ORM\Column(type="integer") | |
*/ | |
private $id; | |
/** | |
* @ORM\Column(type="string") | |
*/ | |
private $username; | |
/** | |
* @ORM\Column(type="string") | |
*/ | |
private $userAvatarFilename; | |
/** | |
* @ORM\Column(type="text") | |
*/ | |
private $note; | |
/** | |
* @ORM\Column(type="datetime") | |
*/ | |
private $createdAt; | |
... lines 39 - 78 | |
} |
Finally, go back to our best friend - the "Code"->"Generate" menu - and generate the getter and setters for every field - except for id
. You don't usually want to set the id
, but generate a getter for it:
... lines 1 - 10 | |
class GenusNote | |
{ | |
... lines 13 - 39 | |
public function getId() | |
{ | |
return $this->id; | |
} | |
public function getUsername() | |
{ | |
return $this->username; | |
} | |
public function setUsername($username) | |
{ | |
$this->username = $username; | |
} | |
public function getUserAvatarFilename() | |
{ | |
return $this->userAvatarFilename; | |
} | |
public function setUserAvatarFilename($userAvatarFilename) | |
{ | |
$this->userAvatarFilename = $userAvatarFilename; | |
} | |
public function getNote() | |
{ | |
return $this->note; | |
} | |
public function setNote($note) | |
{ | |
$this->note = $note; | |
} | |
public function getCreatedAt() | |
{ | |
return $this->createdAt; | |
} | |
public function setCreatedAt($createdAt) | |
{ | |
$this->createdAt = $createdAt; | |
} | |
} |
Entity done! Well, almost done - we still need to somehow relate each GenusNote
to a Genus
. We'll handle that in a second.
But first, don't forget to generate a migration for the new table:
./bin/console doctrine:migrations:diff
Open up that file to make sure it looks right - it lives in app/DoctrineMigrations
. CREATE TABLE genus_note
- it looks great! Head back to the console and run the migration:
./bin/console doctrine:migrations:migrate
Man, that was easy. We'll want some good dummy notes too. Open up the fixtures.yml
file and add a new section for AppBundle\Entity\GenusNote
. Start just like before: genus.note_
and - let's create 100 notes - so use 1..100
:
... lines 1 - 8 | |
AppBundle\Entity\GenusNote: | |
genus.note_{1..100}: | |
... lines 11 - 15 |
Next, fill in each property using the Faker functions: username:
<username()> and then userAvatarFilename:
Ok, eventually users might upload their own avatars, but for now, we have two hardcoded options: leanna.jpeg
and ryan.jpeg
. Let's select one of these randomly with a sweet syntax: 50%?
leanna.jpeg : ryan.jpeg. That's Alice awesomeness:
... lines 1 - 7 | |
AppBundle\Entity\GenusNote: | |
genus.note_{1..100}: | |
username: <userName()> | |
userAvatarFilename: '50%? leanna.jpeg : ryan.jpeg' | |
... lines 13 - 15 |
The rest are easy: note: <paragraph()>
and createdAt:
<dateTimeBetween('-6 months', 'now')>:
... lines 1 - 8 | |
AppBundle\Entity\GenusNote: | |
genus.note_{1..100}: | |
username: <userName()> | |
userAvatarFilename: '50%? leanna.jpeg : ryan.jpeg' | |
note: <paragraph()> | |
createdAt: <dateTimeBetween('-6 months', 'now')> |
Ok, run the fixtures!
./bin/console doctrine:fixtures:load
Double-check them with a query:
./bin/console doctrine:query:sql 'SELECT * FROM genus_note'
So awesome! Ok team, we have two entities: let's add a relationship!
Hey Mike!
Ah, no reason in particular :). I'm *super* accustomed (just from years of habit) to not having fluent setters. I also rarely (but sometimes) call multiple setters in a row. But I definitely don't see any issue with fluent setters!
Cheers!
there
I just noticed, I have a couple of little things that you could integrate into your files to help the viewers in the long run.
create a symlink for bin/console
ln -s bin/console console
add an alias for the console command
(be it on .aliases or your .bash_rc file)
alias console="php console"
type "console"
See the results here http://goo.gl/n1P4CU
oh my terminal? well thats powerline-shell -> https://github.com/milkbiki... <- at work (linux here, works in the mac too for those interested)
also I notice you use php bin/console server:run and not server:start ? any particular reason for it?
I like this stuff - good tips!
And no reason about server:start versus server:run. I feel like new people may have less trouble with server:run, as they may accidentally keep server:start running in the background, then come back later, try to start it again, and get an error.
Cheers!
Hey Luka Sikic
I'm not sure if there is a best practice for it, but for teaching is better to show how you can create a new entity from scratch
Anyways, feel free to create them however you please, as long as you don't introduce a bug :)
Cheers!
Hey Henry,
In Symfony project: just change "database_driver" to "pdo_sqlsrv" instead of "pdo_mysql" in your "app/config/parameters.yml". For custom project based on Doctrine - find a place where configuration parameters are setting according to the Doctrine configuration.
P.S. And also don't forget to tweak other DB credentials as host, port, username and password.
Cheers!
Hi,guys
after load bin/console doctrine:fixtures:load
unexpectedvalueexception could not determine how to assign createAt to a AppBundle\Entity\GenusNote object
Hey Ilya,
Great! I think the problem was that you used `createAt` instead of `createdAt`, right? It should be `createdAt`. Your explanation could help other devs with similar problem.
Cheers!
Hi Guys,
I had the same problem. For me I put a 's' at the end of the class 'GenusNote' => 'GenusNotes'. So It was wrong in the fixtures.yml file.
I have to say many THX for this course. I follow every Symfony 3 lesson. Very very good way to understand this framework before to start à Big project.
In the introductory text it might be an idea to say whether our existing project (from the end of fixture data) is sufficient to begin this course.
Ooh, good tip! In reality, it IS ok to use the existing project... unless I say otherwise (occasionally I make changes between courses, and I explicitly say to use the download code). But, I definitely do not make that crystal clear! We will make this more clear going forward!
Hi KNP!
I'm facing a problem and I can't find a solution, even though I'm following (at least I guess I do) the right "rules" for defining a ManyToMany relation (Category - Filters)
I want to be able to add a list of filters for a specific category, and vice-versa, a list of categories for a specific Filter.
For that I have to following code:
# AppBundle/Entity/Category.php
#...
/**
* @ORM\ManyToMany(targetEntity="Filter", inversedBy="categories")
*/
private $filters;
public function __construct() {
$this->filters = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addFilter(\AppBundle\Entity\Filter $filter) {
$this->filters[] = $filter;
}
public function removeFilter(\AppBundle\Entity\Filter $filter) {
$this->filters->removeElement($filter);
}
public function getFilters() {
return $this->filters;
}
# AppBundle/Entity/Filter.php
#...
/**
* @ORM\ManyToMany(targetEntity="Category", mappedBy="filters")
*/
public $categories;
public function __construct()
{
$this->categories = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addCategory(\AppBundle\Entity\Category $category) {
$category->addFilter($this);
$this->categories[] = $category;
}
public function removeCategory(\AppBundle\Entity\Category $category) {
$this->categories->removeElement($category);
}
public function getCategories() {
return $this->categories;
}
For adding filters to a category, I have this form:
# AppBundle/Entity/CategoryType
#...
->add('filters', EntityType::class, [
'class' => 'AppBundle:Filter',
'placeholder' => ' ',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('f');
},
'choice_label' => function($filters) {
return $filters->getValue();
},
'multiple' => false,
'expanded' => false
])
For adding categories to a filter, I have this form:
->add('categories', EntityType::class, [
'class' => 'AppBundle:Category',
'placeholder' => ' ',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('c');
},
'choice_label' => function($categories) {
return $categories->getName();
},
'multiple' => false,
'expanded' => false
])
When I render any of the two forms, all things goes ok, no problems here.
Questions:
how can I save, for the first time, information in any of the two tables? Because, if I want to add a filter for a category, then the dropdown for this first category is empty - as there are no filters yet. And vice-versa, if I want to add a category for a filter, the dropdown containing categories is empty - as there are no categories yet.
if I comment out the ->add('categories')
from AppBundleForm/FilterType
, and I try to add a new category and a filter for it, then I get this error:
Error 500: Could not determine access type for property "filters".
Any ideas on how to solve this two issues?
Thank you!
Yo Dan Costinel!
Ok, let's do this! First, your setup looks cool - both relationships are setup correctly. In our collections tutorial (which goes into ManyToMany specifically), we do a bit more work in the adder method (you can see an example here: http://knpuniversity.com/sc... to make sure that we can't ever add duplicate links. This isn't your problem, just something to be aware of. Also, adding categories to your filter should work pretty easily, but adding filters to your category will mean that you are setting things from the *inverse* side of the relationship. So, you'll need to do a bit more work with that. But no worries, all stuff we talk about :) http://knpuniversity.com/sc...
Now, let me answer your questions specifically
> 1) how can I save, for the first time, information in any of the two tables?
It depends on your requirements! Should the system start in an empty state? If so, then at some point, the user will need to create a Filter with no Categories or a Category with no Filters in order to start things off. But in many cases, your system might always start with a certain set of Categories (or Filters). In that case, I would add those in your fixtures.
> 2) If I comment out the ->add('categories') from AppBundleForm/FilterType, and I try to add a new category and a filter for it, then I get this error: ...
I think *this* is unrelated to uncommenting ->add('categories') - I think uncommenting that simply allows for your form to submit far enough to hit the error. The error seems to be saying that you have a "filters" form field on your FilterType! Because this is the error you get when a field exists on your form, but no getter/setter/adder/remover exists on the class to handle this. So, double-check the fields you have on your FilterType!
I hope this gets you a step closer!
Cheers!
Thanks for replying Ryan, kind as always!
1) Yeah, the system should start as an empty one. And I was kinda sure about the right answer, which is to choose one of the sides, and create some data for it, and then add data for the other entity. But I asked just to be sure!
2). I create a new test project in symfony, and retyped all again, and still the problem persists, as I get the same error: "Error 500: Could not determine access type for property "filters".", even though I'm not declaring any "filters" form type in my FilterType.php file. Here's a gist of my entire test project: https://gist.github.com/dancostinel/322460b8563b82f29af1462c808703ad .
Firstly, I thought this might be caused because I'm not adding any filter to a category, using the addFilter($category->getFilters())
Category class method. But then I realised the error is thrown previously to the submission (at least this is what I suppose), because it complains at the line which contains $form->handleRequest($request);
(here's the print of the error: http://imgur.com/a/f4WsC )
If you are able to see why this is happening, that would be great!
Thank you!
Yo Dan!
1) Awesome :)
2) Thanks for the code and screenshot! And you're right - this happens when the form is submitted. And I can see from your screenshot that the form component is trying to set a "filters" field on a Category object. So, even though you say this is happening when you submit the FilterType form, it is (somehow) your CategoryType that is causing the problem. Somehow, the CategoryType is being submitted, or is embedded in your form. And actually, I can see this in your exception screenshot - the categoryAction is being executed - not filterAction!
Apart from that, even if your CategoryType *is* being submitted, it *should* in theory work: after all, you *do* have addFilter, removeFilter and getFilters() in Category. I'm honestly not sure what the issue is here - but I would try to add (or uncomment in your case) the by_reference option on your filters field in CategoryType.
I hope this helps find the issue! Cheers!
Hi Ryan.
I think I finally found the problem. It seems that the problem is with that 'multiple' => false
option. If it is set to true
then the whole process works, and only when is set to false throws that error.
Ok, done! But then I'm facing a new problem.
My select is no more a default select, but this: http://imgur.com/a/j2BF2 .
Any tip on how to make this one look like an usual select?
Thanks!
Hey Dan!
Sorry for the late reply! I wanted to reply 2 days ago... but AWS S3 was down, and so was Imgur! But now, I can see your screenshot :).
First, happy the multiple worked! Because your Category is allowed to have an array of filters, this makes sense! I'm not sure why I missed that!
Second, because the Category is allowed to have many filters... it doesn't really make sense to use a normal drop-down select anymore. That's why Symfony is rendering it in this way. If this were a normal drop-down, then you would only be able to choose one filter per Category. But, I also hate this "multi-select" - very not user friendly. If you want to use checkboxes instead, then add an 'expanded' => true
option to the field. If you want to do something even fancier - like some sort of jQuery widget - then you'll probably want to keep the field as it is now, and then point that jQuery widget at the multi-select. Usually, these widgets work by hiding the true element (i.e. the multi-select) and then displaying the fancy widget. But, behind the scenes, when you choose things on the widget, it updates the multi-select.
Let me know if that helps! And cheers!
Hi Ryan.
No problem!
Indeed, I solved the problem regarding the aspect of the drop-down, using "selectize" js plugin.
Thanks for support!
When I load the fixtures I get this :
johnk@johnk-OptiPlex-XE2 /media/johnk/73F9A5F807B5266F/workspace/DoctrineRels $ ./bin/console doctrine:fixtures:load
Careful, database will be purged. Do you want to continue y/N ?y
> purging database
> loading AppBundle\DataFixtures\ORM\LoadFixtures
[Symfony\Component\Debug\Exception\FatalThrowableError]
Call to a member function format() on string
Unfortunately a search of the workspace for format gives over 10,000 results - is there any way of getting more info on where the actual error is located?
Hey jk_scotland!
That's weird, if you want to check the full message, you can pipe everything into a file, something like this (in linux)
$ bin/console d:fix:load --no-interaction > result.txt
Anyways, that error look's like something easy to fix. Can you show me your fixtures.yml?
Cheers!
Found that there was a -v switch that indicated that it was a dateTime problem
Exception trace:
() at /media/johnk/73F9A5F807B5266F/workspace/DoctrineRels/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeType.php:53
And at your suggestion looked in fixtures.yml and found the '>' was missing as the end of 'createdAt: <datetimebetween('-6 months',="" 'now')="">'
Hello Ryan.
How to enable ORM Annotation on code generate menu? In my menu this option inactive.
Thanks.
Hi again!
Hmm, is it inactive, or do you *not* see the option at all? Make sure your cursor is *inside* your entity class when you use the Code->Generate menu.
Let me know what you find out :)
Ah, make sure you have the PHP Annotations plugin installed for PhpStorm. In the first course, I did *not* mention this - we're making an edit to the video to add this missing item!
Just a quick question, in your video, when you create getter and setter for a parameter in GenusNote class, phpStorm is creating getter and setter functions without annotations. But, with my PHPstorm, it is creating annotation for each one.
Below is an example:
```
/**
* @return mixed
*/
public function getUsername()
{
return $this->username;
}
/**
* @param mixed $username
*/
public function setUsername($username)
{
$this->username = $username;
}
```
What are these
/**
* @return mixed
*/
and
/**
* @param mixed $username
*/
?
Do we need them?
Cheers,
Noby
Hey Nobuyuki,
My PhpStorm creates setters and getters with annotation too, so probably Ryan has different settings, or just an older version of PhpStorm, or it just was a bug in PhpStorm :)
This annotation called PHP DocBlock comments. You can read more about it here: https://phpdoc.org/docs/lat... . Basically, this annotations helps IDEs like PhpStorm to do autocompletion. Usually, it's unnecessary for IDE because it can resolve returned type base on @var property annotation. The types of method arguments can be resolved base on typehint, but it don't work for scalar type, that's why @param annotation for methods with scalar values would be good here. But nothing critical won't happen without these annotations for getters/setters, you just don't have an autocompletion in some places of your code.
Actually, here's my recent merged PR to the Symfony Demo project about annotation: https://github.com/symfony/... . I think you might be interested in it - there's a small discussion about annotation best practice.
Cheers!
Hi, Victor
Thank you very much. I understand it better now thanks to your explanation.
Kind regards,
Noby
Yo Maksym,
The <current()>
function return a current index of each iteration. Combine it with username to make it 100% unique.
Cheers!
Do you mean like this, for example?
username: <username()>_<current()>
Well, not the most elegant solution, but... it works. :) I guess it's quite ok for development purposes.
Yes, agree, but I think that's the simplest solution. BTW, I just use username: user<current()>
.
Hi,
When I create an entity class, can I have different names for id like below?
(@ORM\Id and private $connectorId)
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*/
private $connectorId;
Cheers,
Noby
Hey Nobuyuki,
I think you can. Have you tried it? Btw, what about just map $id field to the different column name in database:
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(name="connector_id", type="integer")
*/
private $id;
This should work as well.
Cheers!
Hi, Victor
Thank you for your reply. My first try seemed to have worked. But, will see.
I will try your solution as well.
I have many other questions. so, will ask you later.
Cheers,
Noby
Is there a reason for the Faker fixtures.yml to use "genus.note_{1..100}" instead of "genus_note_{1..100}" or "genusNote_{1..100}" (or in fact "anything_at_all_{1..100}" ?
Clearly it doesn't use this value for a field in the SQL- is it just for an internal Faker ID thing? Thanks!
Hey Goz,
You're right, there's no any difference here, you can use whatever string you want, but of course, it's good when it's relevant to avoid confusing other developers and you later :) Just make sure this key is unique across all your Alice fixtures to be able to refer those objects.
Cheers!
Good morning! Can you help me! when i do 'doctrine:migrations:migrate' I have error and create new table 'Migrating up to 20170421064206 from 0
++ migrating 20170418203924
-> CREATE TABLE genus (id INT AUTO_INCREMENT NOT N....' 'SQLSTATE[42S01]: Base table or view already exists: 1050 Table 'genus' already exists...'
'[Doctrine\DBAL\Driver\PDOException]
SQLSTATE[42S01]: Base table or view alrea
dy exists: 1050 Table 'genus' already exi
sts
[PDOException]
SQLSTATE[42S01]: Base table or view alrea
dy exists: 1050 Table 'genus' already exi
sts...'
Hey Julia,
Well, the reason is simple: as you can see from the error message you already have the `genus` table in your DB, but your migration's trying to create this table. So most likely you're doing something wrong. I find it useful to read the docs for DoctrineMigrationsBundle to understand how it works, some base concepts of migrations and what this bundle provides to you out of the box: https://symfony.com/doc/cur...
In shorts, the main question is *why* this `genus` table already exist on the time you run this migration at the first time? Probably you added this table with `bin/console doctrine:schema:create` or `bin/console doctrine:schema:update --force` command. But if you decided to control your DB with migrations, you have to forget about these create/update DB commands unless you have understanding of how migrations work at least.
For now, to fix this error easy, you can try to remove your DB at all with `bin/console doctrine:database:drop --force`, then create only the DB with `bin/console doctrine:database:create` and after these actions you can start using migrations.
Does it help to fix your error?
Cheers!
Yes! All good! You did it in the next lesson!
bin/console doctrine:database:drop --force
bin/console doctrine:database:create
It helped! Thank you
I try to learn! Excuse me!
Hey Julia,
Great! I'm glad we're on the same wave. And no problem! Actually, it's good that you ask questions even quicker than we explain them in the course, well done!
If you have any questions, do not hesitate to ask again ;)
Cheers!
// 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
}
}
Why aren't you using fluent setters?