If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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:
... 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
.
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
:
... 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:
... 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']
... 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:
... 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()
:
... 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!
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
... 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
:
... 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:
... 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:
... 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!
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:
... 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:
... 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()
:
... 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
:
... 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
:
... 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.
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[]
:
... 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:
... 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 |
"Houston: no signs of life"
Start the conversation!