Buy

Exceptions Part 2: Adding Fence Security

Dinosaurs, check! Enclosures, check! But... we forgot to add security to the enclosures! Ya know, like electric fences! Dang it, I knew we forgot something. The dinosaurs have been escaping their enclosure and... of course, terrorizing the guests. The investors are not going to like this...

Inside Enclosure, add a new securities property: this will be a collection of Security objects - like "fence" security or "watch tower". We'll create that class in a minute. Anyways, if this collection is empty, there's no security! So we cannot let people put dinosaurs here.

49 lines src/AppBundle/Entity/Enclosure.php
... lines 1 - 13
class Enclosure
{
... lines 16 - 21
private $securities;
... lines 23 - 47
}

Let's create another custom exception class: DinosaursAreRunningRampantException. This time, make sure it extends \Exception.

8 lines src/AppBundle/Exception/DinosaursAreRunningRampantException.php
... lines 1 - 4
class DinosaursAreRunningRampantException extends \Exception
{
}

Perfect!

Testing the Dinosaurs don't Run Ramptant

Inside EnclosureTest, add a new method: testItDoesNotAllowToAddDinosToUnsecureEnclosures. And yea... this is pretty simple: just create the new Enclosure(), and then add the dinosaur. But first, this time, I want to test for the exception class and exception message. You can do both via annotations, or right here with $this->expectException(DinosaursAreRunningRamptantException::class) and $this->expectExceptionMessage('Are you craaazy?!?').

Below that, add a new Dinosaur().

62 lines tests/AppBundle/Entity/EnclosureTest.php
... lines 1 - 10
class EnclosureTest extends TestCase
{
... lines 13 - 51
public function testItDoesNotAllowToAddDinosToUnsecureEnclosures()
{
$enclosure = new Enclosure();
$this->expectException(DinosaursAreRunningRampantException::class);
$this->expectExceptionMessage('Are you craaazy?!?');
$enclosure->addDinosaur(new Dinosaur());
}
}

Nice! Except... yea... this is going to make all of our tests fail: none of those Enclosures have security. Let's worry about that later: focus on this test.

In fact, copy the method name and find your terminal. To avoid noise, I want to run only this one test. You can do that with ./vendor/bin/phpunit --filter and then the method:

./vendor/bin/phpunit --filter testItDoesNotAllowToAddDinosToUnsecureEnclosures

Awesome! One test and one failure. We'll talk more about --filter soon!

Adding the Security Class

Ok, step 2 of TDD: code! First, we need a Security class. If you downloaded the course code, you should have a tutorial/ directory with a Security class inside. Paste that into our Entity directory.

47 lines src/AppBundle/Entity/Security.php
... lines 1 - 2
namespace AppBundle\Entity;
... line 4
use Doctrine\ORM\Mapping as ORM;
... line 6
/**
* @ORM\Entity
* @ORM\Table(name="securities")
*/
class Security
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string")
*/
private $name;
/**
* @ORM\Column(type="boolean")
*/
private $isActive;
/**
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\Enclosure", inversedBy="securities")
*/
private $enclosure;
public function __construct(string $name, bool $isActive, Enclosure $enclosure)
{
$this->name = $name;
$this->isActive = $isActive;
$this->enclosure = $enclosure;
}
public function getIsActive(): bool
{
return $this->isActive;
}
}

It's pretty simple: it has a name, an isActive boolean and a reference to the Enclosure it's attached to.

Speaking of Enclosure, initialize its $securities property to a new ArrayCollection. Oh, and on the property, add @var Collection|Security[].

79 lines src/AppBundle/Entity/Enclosure.php
... lines 1 - 14
class Enclosure
{
... lines 17 - 22
/**
* @var Collection
... line 25
*/
private $securities;
public function __construct(bool $withBasicSecurity = false)
{
... line 31
$this->securities = new ArrayCollection();
... lines 33 - 36
}
... lines 38 - 77
}

Really, this will be a Collection instance. But the Security[] part tells our editor that this is a collection of Security objects. And that will give us better auto-completion. Which we can only enjoy if we get this dino security going, so let's get to it!

Throwing the Exception

Down in addDinosaur(), we need to know if this Enclosure has at least one active security. Add a method to help with that: public function isSecurityActive(). I'm making this public only because I already know I'm going to use it later outside of this class.

Set this to return a bool and then loop! Iterate over $this->securities as $security. And if $security->getIsActive(), return true. If there are no active securities, run for your life! And also return false at the bottom.

79 lines src/AppBundle/Entity/Enclosure.php
... lines 1 - 14
class Enclosure
{
... lines 17 - 67
public function isSecurityActive(): bool
{
foreach ($this->securities as $security) {
if ($security->getIsActive()) {
return true;
}
}
return false;
}
}

Finish things in addDinosaur(): if not $this->isSecurityActive(), throw a new DinosaursAreRunningRampantException(). And remember, we're checking for an exact message: so use the string from the test.

79 lines src/AppBundle/Entity/Enclosure.php
... lines 1 - 14
class Enclosure
{
... lines 17 - 43
public function addDinosaur(Dinosaur $dinosaur)
{
if (!$this->isSecurityActive()) {
throw new DinosaursAreRunningRampantException('Are you craaazy?!?');
}
... lines 49 - 54
}
... lines 56 - 77
}

Ok, I think we're done! Go tests go!

./vendor/bin/phpunit --filter testItDoesNotAllowToAddDinosToUnsecureEnclosures

Yes! It's now impossible to add a Dinosaur to an Enclosure... unless there's some security to keep it inside.

Adding Annotations

We've reached the third step of TDD once again: refactor. Actually, I don't need to refactor, but now is a great time to add the missing Doctrine annotations. Above the $securities property, add @ORM\OneToMany() with targetEntity="Security" and mappedBy="enclosure". enclosure is the name of the property on the other side of the relation. Finish it with cascade={"persist"}.

79 lines src/AppBundle/Entity/Enclosure.php
... lines 1 - 14
class Enclosure
{
... lines 17 - 22
/**
... line 24
* @ORM\OneToMany(targetEntity="AppBundle\Entity\Security", mappedBy="enclosure", cascade={"persist"})
*/
private $securities;
... lines 28 - 77
}

Ok, this one test still passes... but what about the rest? Run them:

./vendor/bin/phpunit

Ah! We have so many DinosaursAreRunningRampantException errors! Yep, we knew this was coming: our existing tests need security.

To make this easy, inside Enclosure, add a bool $withBasicSecurity argument. Then, if this is true, let's add some basic security! $this->addSecurity() - we'll create this method next - new Security('Fence', true) - for isActive - and then $this for the Enclosure.

79 lines src/AppBundle/Entity/Enclosure.php
... lines 1 - 14
class Enclosure
{
... lines 17 - 28
public function __construct(bool $withBasicSecurity = false)
{
... lines 31 - 33
if ($withBasicSecurity) {
$this->addSecurity(new Security('Fence', true, $this));
}
}
... lines 38 - 77
}

Add the missing method public function addSecurity(), append the securities array and... we're done!

79 lines src/AppBundle/Entity/Enclosure.php
... lines 1 - 14
class Enclosure
{
... lines 17 - 56
public function addSecurity(Security $security)
{
$this->securities[] = $security;
}
... lines 61 - 77
}

Inside EnclosureTest, the first test method does not need security: it never adds any dinosaurs. But the next three do: pass true, true and true.

62 lines tests/AppBundle/Entity/EnclosureTest.php
... lines 1 - 10
class EnclosureTest extends TestCase
{
... lines 13 - 19
public function testItAddsDinosaurs()
{
$enclosure = new Enclosure(true);
... lines 23 - 27
}
... line 29
public function testItDoesNotAllowCarnivorousDinosToMixWithHerbivores()
{
$enclosure = new Enclosure(true);
... lines 33 - 38
}
... lines 40 - 43
public function testItDoesNotAllowToAddNonCarnivorousDinosaursToCarnivorousEnclosure()
{
$enclosure = new Enclosure(true);
... lines 47 - 49
}
... lines 51 - 60
}

Let's do the tests!

./vendor/bin/phpunit

Yes! This is amazing! We just created a dinosaur park with security. What a novel idea!

Leave a comment!