Buy

Ships are loading dynamically, buuuuuut, I've got some bad news: we broke our app. Start a battle - select the Jedi Starfighter as one of the ships and engage.

Huh, so instead of the results, we see:

Don't forget to select some ships to battle!

Pretty sure we selected a ship... But the URL has a ?error=missing_data part, index.php is reading this. It all comes from battle.php and it happens if we POST here, but we are missing ship1_name or ship2_name. In other words, if we forget to select a ship. But we did select a ship! Somehow, these select menus are broken. Check out the code: we're looping over $ships and using $key as the option value:

119 lines index.php
... lines 1 - 90
<select class="center-block form-control btn drp-dwn-width btn-default btn-lg dropdown-toggle" name="ship1_name">
... line 92
<?php foreach ($ships as $key => $ship): ?>
<?php if ($ship->isFunctional()): ?>
<option value="<?php echo $key; ?>"><?php echo $ship->getNameAndSpecs(); ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
... lines 99 - 119

In getShips(), the key was a nice, unique string. But now it's just the auto-increment index. The page fails because the 0 index looks like an empty string in battle.php.

Adding a Ship id Property

We still need something unique so that we can tell battle.php exactly which ships are fighting. Fortunately, the ship table has exactly that: an auto-incrementing primary key id column. If we use this as the option value, we can query for the ships using that in battle.php. Blast off! I mean, we should totally do that.

In ShipLoader, we could put the id as the key of the array. But instead, since id is a column on the ship table, why not also make it a property on the Ship class? Open up Ship and add a new private $id:

135 lines lib/Ship.php
... lines 1 - 2
class Ship
{
private $id;
... lines 6 - 133
}

And at the bottom, right click, then make the getter and setter for the id property. Update the PHPDoc to show that $id is an integer. Optional, but nice:

135 lines lib/Ship.php
... lines 1 - 2
class Ship
{
... lines 5 - 118
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @param int $id
*/
public function setId($id)
{
$this->id = $id;
}
}

Now when we get our Ship objects, we need to call setId() to populate that property: $ship->setId() and $shipData['id']

34 lines lib/ShipLoader.php
... lines 1 - 2
class ShipLoader
{
public function getShips()
{
... lines 7 - 10
foreach ($shipsData as $shipData) {
$ship = new Ship($shipData['name']);
$ship->setId($shipData['id']);
... lines 14 - 17
$ships[] = $ship;
... lines 19 - 21
}
... lines 23 - 33
}

Head over to index.php to use the fancy new property. Remove the $key in the foreach - no need for that. And instead of the key, print $ship->getId(). Also change the select name to be ship1_id so we don't get confused about what this value is:

119 lines index.php
... lines 1 - 90
<select class="center-block form-control btn drp-dwn-width btn-default btn-lg dropdown-toggle" name="ship1_id">
... line 92
<?php foreach ($ships as $ship): ?>
<?php if ($ship->isFunctional()): ?>
... lines 95 - 96
<?php endforeach; ?>
</select>
... lines 99 - 119

Make the same changes below: update the select name, remove $key from the loop, and finish with $ship->getId():

119 lines index.php
... lines 1 - 102
<select class="center-block form-control btn drp-dwn-width btn-default btn-lg dropdown-toggle" name="ship2_id">
... line 104
<?php foreach ($ships as $ship): ?>
... line 106
<option value="<?php echo $ship->getId(); ?>"><?php echo $ship->getNameAndSpecs(); ?></option>
... line 108
<?php endforeach; ?>
... lines 110 - 119

Ok, before we touch battle, try this out. No errors! And the select items have values 1, 2, 3 and 4 - the auto-increment ids in the database. Success!

Querying for One Ship

We've renamed the select fields and we're sending a database id. Let's update battle.php for this. First, we need to change the $_POST keys: look for ship1_id and ship2_id. Update the variables names too - $ship1Id and $ship2Id. That'll help us not get confused. Update the variables in the first if statement

106 lines battle.php
... lines 1 - 6
$ship1Id = isset($_POST['ship1_id']) ? $_POST['ship1_id'] : null;
... line 8
$ship2Id = isset($_POST['ship2_id']) ? $_POST['ship2_id'] : null;
... lines 10 - 11
if (!$ship1Id || !$ship2Id) {
header('Location: /index.php?error=missing_data');
die;
}
... lines 16 - 106

Before, we got all the $ships then used the array key to find the right ones. That won't work anymore - the key is just an index, but we have the id from the database.

Instead, we can use that id to query for a single ship's data. Where should that logic live? In ShipLoader! It's only job is to query for ship information, so it's perfect.

Create a new public function findOneById() with an $id argument. Copy all the query logic from queryForShips() and put it here. For now don't worry about all this ugly code duplication. Update the query to be SELECT * FROM ship WHERE id = :id and pass that value to execute() with an array of id to $id:

45 lines lib/ShipLoader.php
... lines 1 - 2
class ShipLoader
{
... lines 5 - 23
public function findOneById($id)
{
$pdo = new PDO('mysql:host=localhost;dbname=oo_battle', 'root');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$statement = $pdo->prepare('SELECT * FROM ship WHERE id = :id');
$statement->execute(array('id' => $id));
... lines 30 - 32
}
... lines 34 - 45

If this looks weird to you - it's a prepared statement. It runs a normal query, but prevents SQL injection attacks. Change the variable below to be $shipArray and change fetchAll() to just fetch() to return the one row. Dump this at the bottom:

45 lines lib/ShipLoader.php
... lines 1 - 23
public function findOneById($id)
{
$pdo = new PDO('mysql:host=localhost;dbname=oo_battle', 'root');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$statement = $pdo->prepare('SELECT * FROM ship WHERE id = :id');
$statement->execute(array('id' => $id));
$shipArray = $statement->fetch(PDO::FETCH_ASSOC);
var_dump($shipArray);die;
}
... lines 34 - 45

Ok, back to battle.php! Let's use this. Now, $ship1 = $shipLoader->findOneById($ship1Id). And $ship2 = $shipLoader->findOneById($ship2Id). And I need to move this code further up above the bad_ships error message. We'll use it in a second:

106 lines battle.php
... lines 1 - 16
$ship1 = $shipLoader->findOneById($ship1Id);
$ship2 = $shipLoader->findOneById($ship2Id);
... lines 19 - 106

Try it! Fight some Starfighters against a Cloakshape Fighter. There's the dump for just one row! Sweet, let's finish this!

Going from Array to Ship Object

The last step is to take this array and turn it into a Ship object. And good news! We've already done this in getShips()! And instead of repeating ourselves, this is another perfect spot for a private function. Create one called createShipFromData with an array $shipData argument:

57 lines lib/ShipLoader.php
... lines 1 - 2
class ShipLoader
{
... lines 5 - 32
private function createShipFromData(array $shipData)
{
... lines 35 - 41
}
... lines 43 - 54
}
... lines 56 - 57

Copy all the new Ship() code and paste it here. Return the $ship variable:

57 lines lib/ShipLoader.php
... lines 1 - 32
private function createShipFromData(array $shipData)
{
$ship = new Ship($shipData['name']);
$ship->setId($shipData['id']);
$ship->setWeaponPower($shipData['weapon_power']);
$ship->setJediFactor($shipData['jedi_factor']);
$ship->setStrength($shipData['strength']);
return $ship;
}
... lines 43 - 57

Now, anyone inside ShipLoader can call this, pass an array from the database, and get back a fancy new Ship object.

Back in getShips(), remove all that code and just use $this->createShipFromData(). Do the same thing in findOneById():

57 lines lib/ShipLoader.php
... lines 1 - 4
public function getShips()
{
... lines 7 - 10
foreach ($shipsData as $shipData) {
$ships[] = $this->createShipFromData($shipData);
}
... lines 14 - 15
}
... line 17
public function findOneById($id)
{
$pdo = new PDO('mysql:host=localhost;dbname=oo_battle', 'root');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$statement = $pdo->prepare('SELECT * FROM ship WHERE id = :id');
$statement->execute(array('id' => $id));
$shipArray = $statement->fetch(PDO::FETCH_ASSOC);
... lines 25 - 29
return $this->createShipFromData($shipArray);
}
... lines 32 - 57

In battle.php, $ship1 and $ship2 should now be Ship objects. The next if statement is a way to make sure that valid ship ids were passed: maybe someone is messing with our form! With these tough ships in my database I should hope not.

I still want this check, so back in ShipLoader, add one more thing. If the id is invalid - like 10 or the word "pirate ship" - then $shipArray will be null. So, if (!$shipArray) then just return null:

57 lines lib/ShipLoader.php
... lines 1 - 17
public function findOneById($id)
{
... lines 20 - 23
$shipArray = $statement->fetch(PDO::FETCH_ASSOC);
if (!$shipArray) {
return null;
}
return $this->createShipFromData($shipArray);
}
... lines 32 - 57

The method now returns a Ship object or null. Back in battle.php, update the if to say if !$ship1 || !$ship2:

106 lines battle.php
... lines 1 - 16
$ship1 = $shipLoader->findOneById($ship1Id);
$ship2 = $shipLoader->findOneById($ship2Id);
if (!$ship1 || !$ship2) {
header('Location: /index.php?error=bad_ships');
die;
}
... lines 24 - 106

And that should do it!

Go back and load the homepage fresh. And start a battle. When we submit, we'll be POST'ing these 2 ids to battle.php. And it works!

Thanks to ShipLoader, everyone is talking to the database, but nobody has to really worry about this.

PHPDoc for Autocomplete!

Let's fix one little thing that's bothering me. In index.php, we call getShips(). But when we loop over $ships, PhpStorm acts like all of the methods on the Ship object don't exist: getName not found in class.

If you look above getShips(), there's no PHP documentation. And so PhpStorm has no idea what this function returns. To fix that, add the /** above it and hit enter to generate some basic docs. Now it says @return array. That's true, but it doesn't tell it what's inside the array. Change it to @return Ship[]:

64 lines lib/ShipLoader.php
... lines 1 - 2
class ShipLoader
{
/**
* @return Ship[]
*/
public function getShips()
{
... lines 10 - 18
}
... lines 20 - 61
}
... lines 63 - 64

This says: "I return an array of Ship objects". And when we loop over something returned by getShips(), we get happy code completion. Do the same thing above findOneById() - it returns just one Ship or null:

64 lines lib/ShipLoader.php
... lines 1 - 2
class ShipLoader
{
... lines 5 - 20
/**
* @param $id
* @return Ship
*/
public function findOneById($id)
{
$pdo = new PDO('mysql:host=localhost;dbname=oo_battle', 'root');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$statement = $pdo->prepare('SELECT * FROM ship WHERE id = :id');
$statement->execute(array('id' => $id));
$shipArray = $statement->fetch(PDO::FETCH_ASSOC);
if (!$shipArray) {
return null;
}
return $this->createShipFromData($shipArray);
}
... lines 39 - 61
}
... lines 63 - 64

Leave a comment!