Buy

Void Types & Refactoring an Entire Class

There's one last feature with return types: void return types. setFunFact() is a function... but it doesn't return anything. You can advertise that with : void. This literally means that the method returns... nothing. And not surprisingly, when we refresh, it works great... because we are in fact not returning anything.

226 lines src/AppBundle/Entity/Genus.php
<?php
declare(strict_types = 1);
... line 3
namespace AppBundle\Entity;
... lines 5 - 17
class Genus
{
... lines 20 - 135
public function setFunFact(?string $funFact): void
{
... lines 138 - 140
}
... lines 142 - 224
}

But now, try to return null. This will not work! When your return type is void, it literally means you do not return anything. Even null is not allowed.

226 lines src/AppBundle/Entity/Genus.php
<?php
... lines 2 - 17
class Genus
{
... lines 20 - 135
public function setFunFact(?string $funFact): void
{
$this->funFact = $funFact;
return null;
}
... lines 142 - 224
}

The void return type isn't that important, but it's useful because (A), it documents that this method does not return anything and (B) it guarantees that we don't get crazy and accidentally return something.

Oh, but you can use the return statement - as long as it it's just return with nothing after.

226 lines src/AppBundle/Entity/Genus.php
<?php
... lines 2 - 17
class Genus
{
... lines 20 - 135
public function setFunFact(?string $funFact): void
{
$this->funFact = $funFact;
return;
}
... lines 142 - 224
}

Updating all of Genus

Great news! We now know everything about scalar type hints and return types. And we can use our new super powers to update everything in Genus.

Let's do it! Start with getId(), this will return an int, but it should be nullable: there is no id at first. For getName(), the same thing, ?string. For setName(), it's up to you: this accepts a string, but I am going to allow it to be null. But if you never want that to happen, don't allow it! And of course, the method should return void.

224 lines src/AppBundle/Entity/Genus.php
<?php
... lines 2 - 18
class Genus
{
... lines 21 - 93
public function getId(): ?int
... lines 95 - 98
public function getName(): ?string
... lines 100 - 103
public function setName(?string $name): void
... lines 105 - 222
}

For getSubFamily(), this is easy: it will return a SubFamily object or null. The cool part is that we don't need the PHP doc anymore! We have a return type! Amazing!

224 lines src/AppBundle/Entity/Genus.php
<?php
... lines 2 - 18
class Genus
{
... lines 21 - 108
public function getSubFamily(): ?SubFamily
... lines 110 - 222
}

For setSubFamily(), mark this to return void. Notice that the argument is SubFamily $subFamily = null. If we want that argument to be required, we could change that to ?SubFamily. Your call!

224 lines src/AppBundle/Entity/Genus.php
<?php
... lines 2 - 18
class Genus
{
... lines 21 - 113
public function setSubFamily(SubFamily $subFamily = null): void
... lines 115 - 222
}

Let's keep going! getSpeciesCount() returns a nullable int, though we could give that a default value of 0 if we want, and remove the question mark. setSpeciesCount() accepts a nullable int and returns void. Again, the nullable part is up to you.

224 lines src/AppBundle/Entity/Genus.php
<?php
... lines 2 - 18
class Genus
{
... lines 21 - 118
public function getSpeciesCount(): ?int
... lines 120 - 123
public function setSpeciesCount(?int $speciesCount): void
... lines 125 - 222
}

For getUpdatedAt(), set its return type to a nullable DateTimeInterface, because this starts as null. But notice... I'm returning a DateTime object... so why make the return-type DateTimeInterface? Well, it's up to you. With this type-hint, I could update my code later to return a DateTimeImmutable object. But more importantly, the return type is what you're "advertising" to outsiders. Choose whatever makes the most sense.

224 lines src/AppBundle/Entity/Genus.php
<?php
... lines 2 - 18
class Genus
{
... lines 21 - 140
public function getUpdatedAt(): \DateTimeInterface
... lines 142 - 222
}

Ok, let's get this done! setIsPublished takes a bool that is not nullable, and it returns void. getIsPublished will definitely return a bool - we initialized it to a bool when we defined the property.

224 lines src/AppBundle/Entity/Genus.php
<?php
... lines 2 - 18
class Genus
{
... lines 21 - 145
public function setIsPublished(bool $isPublished): void
... lines 147 - 150
public function getIsPublished(): bool
... lines 152 - 222
}

For getNotes(), return a Collection and then update the PHPDoc to match. There are two interesting things happening. First, ArrayCollection implements this Collection interface, so using the interface is a bit more flexible. Normally, that's just a choice you can make: set your return type to the class you know you're returning... or use the more flexible interface. But actually, for Doctrine collections, you must use Collection. Depending on the situation, this property might be an ArrayCollection or a PersistentCollection... both of which implement the Collection interface. In other words, the only guarantee we can make is that this returns the Collection interface.

224 lines src/AppBundle/Entity/Genus.php
<?php
... lines 2 - 18
class Genus
{
... lines 21 - 155
/**
* @return Collection|GenusNote[]
*/
public function getNotes(): Collection
... lines 160 - 222
}

Second, this returns a collection of GenusNote objects. In other words, if you call getNotes() and then loop over the results, each item will be a GenusNote. But there's no way to denote that with return types. That's why we're keeping the |GenusNote[]. That helps my editor when looping.

getFirstDiscoveredAt() returns a nullable DateTimeInterface and setFirstDiscoveredAt() returns void. getSlug() will be a nullable string, setSlug() will accept a nullable string argument and return void. addGenusScientists() will return void and removeGenusScientist() the same. For getGenusScientists(), like before, I'll set the return type to Collection and update the PHP doc. Do the same for getExpertScientists(): return Collection. This PHPDoc is already correct... but I'll shorten it.

224 lines src/AppBundle/Entity/Genus.php
<?php
... lines 2 - 18
class Genus
{
... lines 21 - 163
public function getFirstDiscoveredAt(): ?\DateTimeInterface
... lines 165 - 168
public function setFirstDiscoveredAt(\DateTime $firstDiscoveredAt = null): void
... lines 170 - 173
public function getSlug(): ?string
... lines 175 - 178
public function setSlug(?string $slug): void
... lines 180 - 183
public function addGenusScientist(GenusScientist $genusScientist): void
... lines 185 - 194
public function removeGenusScientist(GenusScientist $genusScientist): void
... lines 196 - 205
/**
* @return Collection|GenusScientist[]
*/
public function getGenusScientists(): Collection
... lines 210 - 213
/**
* @return \Doctrine\Common\Collections\Collection|GenusScientist[]
*/
public function getExpertScientists(): Collection
... lines 218 - 222
}

Phew! That makes our class a lot tighter: it's now more readable and more difficult to make mistakes. But it also took some work! The cool thing is that you have the power to add return types and type-hint arguments wherever you want. But you don't need to do it everywhere.

After all those changes... did we break anything? Find your browser and go to /genus/new. This is a "dummy" URL that creates and saves a Genus behind the scenes. So apparently, that still works! Click the Genus to go to its show page. Then, login using weaverryan+1@gmail.com and password iliketurtles. Once you do that, click to edit the genus.

Let's see... change the species to 5000, keep the fun fact empty and change the name. Hit enter!

Yay! Everything still works! And our Genus class is awesome!

Leave a comment!

  • 2018-04-10 Victor Bocharsky

    Hey Virginie,

    Hm, let me see again... as I understand, you're mostly talking about this:
    > Apparently, the SubFamily isn't correctly retrieve on line 44 in the GenusController.
    Why do you think it's retrieved incorrectly? I see:


    $subFamily = $em->getRepository(SubFamily::class)
    ->findAny('AppBundle:SubFamily');


    This is totally valid as for me.

    > I also have a PhpStorm warning on this line, for the findAny() method, a method not found warning
    Make sense of course, because getRepository() method has "@return \Doctrine\Common\Persistence\ObjectRepository" where there're no any findAny() method, this method is from custom SubFamilyRepository. Well, If you enable and configure Symfony Plugin for PhpStorm - you won't see this warning. Or, if you don't want to use this plugin - you can manually help PhpStorm to understand better what getRepository() method returns. For example, just add an annotation manually and you won't see this warning:


    /** @var \AppBundle\Repository\SubFamilyRepository $subFamilyRepo */
    $subFamilyRepo = $em->getRepository(SubFamily::class);
    $subFamilyRepo->findAny('AppBundle:SubFamily');

    > In the SubFamilyRepository, I have aPhpStorm warning in the findAny() method as an unhandled UniqueResultException.
    Yes, this is a new feature for PhpStorm. This exception will be thrown if your query returns more than one row in getOneOrNullResult(), and we really do not want to catch and handle this error because this should never happen since we call setMaxResults(1).

    > At the end, I still don't get any result for a SubFamily in the GenusController, thus, I have this error.
    This one is interesting... Do you think we missed something in this screencast or this is just your error, i.e. you just missed something when following our tutorials? Do you have an idea how we can improve this? Because for me it looks like you just forgot to load fixtures that's why findAny() returns null for you, and it seems like a user error.

    Would be glad to make a quick fix if there's really a missing part in our process.

    Cheers!

  • 2018-04-06 Virginie Burlot

    I don't think it's intentional as it goes smoothly in the video, without any mention about any fix I should make for the code to run. It isn't blocking for the learning process, as I already knew SF, I could figure out why there was this issue, however, if someone who only wants to know about PHP7, without any prior knowledge on SF, has the same issue, I think he/she would be lost.

  • 2018-03-26 Victor Bocharsky

    Hey Virginie,

    I think it's not a complete error you see, howeverI suppose you see it because sub_family_id value is null, but from the code I see nullable=false for Genus::$subFamily . So you need to find out why it is null. Is it intentionally? I think if you fixed the problem why it's null - the error will be gone.

    Cheers!

  • 2018-03-23 Virginie Burlot

    Hi ! Thank you again for your great course. After years of PHP5, it's nice to have a nice way to keep updated !
    I have one issue while loading the genus/new route.

    Here is the SF error I got :

    An exception occurred while executing 'INSERT INTO genus (name,
    slug, species_count, fun_fact, is_published, first_discovered_at,
    sub_family_id) VALUES (?, ?, ?, ?, ?, ?, ?)' with params ["Octopus5954",
    "octopus5954", 30496, null, 1, "2068-03-23", null]:

    Apparently, the SubFamily isn't correctly retrieve on line 44 in the GenusController. I also have a PhpStorm warning on this line, for the findAny() method, a method not found warning. In the SubFamilyRepository, I have aPhpStorm warning in the findAny() method as an unhandled UniqueResultException. At the end, I still don't get any result for a SubFamily in the GenusController, thus, I have this error.

  • 2017-10-30 weaverryan

    Yo Mark Henriksen!

    I'll jump in here too... since I'm to blame ;). Yes, we often use that auto-complete PHP Storm trick. It's difficult because if we explain it each time, it will feel laborious after the first time... but if you haven't seen it before, it's surprising! I try to explain it when it makes sense, but it's quite imperfect :/.

    About the speed, it may not always help (because when we're just talking, you may want full speed), but the video player also has a speed option. Oh, and one more thing! The code blocks are below the videos, so you can reference the code there too if it helps :).

    Cheers!

  • 2017-10-27 Diego Aguiar

    Hey Mark Henriksen

    I know, is not so easy to code along with the video without pausing, but maybe an extra monitor could help you get faster :)

    About the login issue. I believe Ryan had in his session a redirection to the admin area after a successfully login. Symfony does it by default when you try to access a secured are when you are anonymous.

    Cheers!

  • 2017-10-27 Mark Henriksen

    @ 4:00 when you set Collection, it went WAY too fast, that you actually used phpStorm Auto-complete to add use statement for Doctrine\Common\Collection.

    Am I the only one, that thinks these tutoials sometimes write too fast? trying to keep up, I have to keep pausing video

    + Last step for Login, does not bring you to genus overview page.