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

Exceptions Part 2: Adding Fence Security

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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.

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

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

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

... 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[].

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

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

... 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"}.

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

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

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

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

9
Login or Register to join the conversation
Sebastian R. Avatar
Sebastian R. Avatar Sebastian R. | posted 3 years ago | edited

I have tried to execute phpUnit through > php ./vendor/bin/simple-phpunit tests/appBundle/Controllerbut i recive:

`

if [ -d /proc/cygdrive ]; then

case $(which php) in
    $(readlink -n /proc/cygdrive)/*)
        # We are in Cygwin using Windows php, so the path must be translated
        dir=$(cygpath -m "$dir");
        ;;
esac

fi

"${dir}/simple-phpunit" "$@"

C:\xampp\htdocs\doki\dev2>php ./vendor/bin/simple-phpunit tests/appBundle/Controller

dir=$(cd "${0%[/\]*}" > /dev/null; cd "../symfony/phpunit-bridge/bin" && pwd)

if [ -d /proc/cygdrive ]; then

case $(which php) in
    $(readlink -n /proc/cygdrive)/*)
        # We are in Cygwin using Windows php, so the path must be translated
        dir=$(cygpath -m "$dir");
        ;;
esac

fi

"${dir}/simple-phpunit" "$@"

C:\xampp\htdocs\doki\dev2>php ./vendor/bin/simple-phpunit tests/appBundle/Controller/SeoControllerTest.php

dir=$(cd "${0%[/\]*}" > /dev/null; cd "../symfony/phpunit-bridge/bin" && pwd)

if [ -d /proc/cygdrive ]; then

case $(which php) in
    $(readlink -n /proc/cygdrive)/*)
        # We are in Cygwin using Windows php, so the path must be translated
        dir=$(cygpath -m "$dir");
        ;;
esac

fi

"${dir}/simple-phpunit" "$@"`

Reply
Sebastian R. Avatar
Sebastian R. Avatar Sebastian R. | posted 3 years ago

I have a little question, if i cant execute phpUnit with .vendor/bin/phpUnit, what can i do it?

Reply

Hey Sebastian R.

Seems like your local machine is not executing the phpunit binary. Have you tried running it like this:
php vendor/bin/phpunit (without ./ at the begining)
Another thing that may be the problem is the file permissions. Check that your user can execute such file

Cheers!

1 Reply
Sebastian R. Avatar
Sebastian R. Avatar Sebastian R. | MolloKhan | posted 3 years ago

excuseme.

In what file need i to change my permissions?

Reply

to the file vendor/bin/phpunit.

1 Reply
Lijana Z. Avatar
Lijana Z. Avatar Lijana Z. | posted 5 years ago

Is this good practice to add such check in the constructor? I never add such, unless somebody forces me to do so.

Reply

Hey Lijana Z.

Do you mean the `$withBasicSecurity` check? If that's the case, I would say yes because it is part of the initialization process and it's pretty simple

Cheers!

Reply
Lijana Z. Avatar

yes, I mean this.

Reply

I would go with that approach, but if in the future that security check necessitates more logic, then you can refactor it into it's own service, or depends on your requirements, you may want to apply a design pattern, like the "visitor" or "builder" pattern.

Reply
Cat in space

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

While the fundamentals of PHPUnit haven't changed, this tutorial *is* built on an older version of Symfony and PHPUnit.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.0, <7.4",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/orm": "^2.5", // v2.7.2
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "sensio/distribution-bundle": "^5.0.19", // v5.0.21
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.28
        "symfony/monolog-bundle": "^3.1.0", // v3.1.2
        "symfony/polyfill-apcu": "^1.0", // v1.6.0
        "symfony/swiftmailer-bundle": "^2.3.10", // v2.6.7
        "symfony/symfony": "3.3.*", // v3.3.13
        "twig/twig": "^1.0||^2.0" // v2.4.4
    },
    "require-dev": {
        "doctrine/data-fixtures": "^1.3", // 1.3.3
        "doctrine/doctrine-fixtures-bundle": "^2.3", // v2.4.1
        "liip/functional-test-bundle": "^1.8", // 1.8.0
        "phpunit/phpunit": "^6.3", // 6.5.2
        "sensio/generator-bundle": "^3.0", // v3.1.6
        "symfony/phpunit-bridge": "^3.0" // v3.4.30
    }
}
userVoice