Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

OO Best Practice: Centralizing Configuration

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 $6.00

Ok, next problem: at the bottom of ShipLoader, our database connection information is hardcoded. That's a problem for two reasons. First, if this works on my computer, it probably won't work on production, unless everything matches up. And second, what if we need a database connection inside some other class? Right now, we'd just have to copy and paste those credentials into yet another spot. Eww.

Here's the goal: move the database configuration out of this class to somewhere more central so it can be re-used. And good news: the way you do this is fundamentally important to using object-oriented code correctly.

How to Make the OO Kittens Sad

But first, let me tell you what you shouldn't do. You shouldn't just move this configuration to another file and then use some global keywords to get that information here. You will see this kind of stuff - heck you might see it all the time depending on your project. The problem is that your code gets harder to read and maintain: "Hey, where the heck is this $dbPassword" variable created? And what if you wanted to re-use this class in another project? It better have global variables with the exact same names.

Learning the better way is the difference between an "ok" object-oriented developer and a great one: and even though this is only episode 2, you're about to learn it.

The Secret: Pass Objects the Config they Need

The secret is this: if a service class - like ShipLoader - needs information - like a database password - we need to pass that information to ShipLoader instead of expecting it to use a global keyword or some other method to "find" it on its own. The most common way to do this is by creating a constructor.

Create a Constructor for Options

Create a public function __construct() and make an argument for each piece of configuration this class needs. ShipLoader needs three pieces of configuration. First, the database DSN - which is the connection parameter, thing mysql:host=localhost. It also needs the $dbUser and the $dbPassword:

... lines 1 - 2
class ShipLoader
{
... lines 5 - 10
public function __construct($dbDsn, $dbUser, $dbPass)
{
... lines 13 - 15
}
... lines 17 - 82
}
... lines 84 - 85

And just like any class, you'll set each of these on a private property. Create a private $dbDsn, $dbUser and $dbPass. In __construct(), assign each argument to the property. I made my arguments - like $dbUser the same as my property name - but that's not needed, it's just nice for my own sanity:

... lines 1 - 2
class ShipLoader
{
... lines 5 - 6
private $dbDsn;
private $dbUser;
private $dbPass;
public function __construct($dbDsn, $dbUser, $dbPass)
{
$this->dbDsn = $dbDsn;
$this->dbUser = $dbUser;
$this->dbPass = $dbPass;
}
... lines 17 - 82
}
... lines 84 - 85

If this feels silly, pointless or you don't get it yet. That's GREAT. Keep watching. Thanks to this change, whoever creates a new ShipLoader() is forced to pass in these 3 configuration arguments. We don't care who creates ShipLoader, but when they do, we store the configuration on three properties and can use that stuff in our methods below.

At the bottom - let's do that. Copy the long database DSN string from new PDO() and replace it with $this->dbDsn. Make the second argument $this->dbUser and the third $this->dbPass:

... lines 1 - 2
class ShipLoader
{
... lines 5 - 64
private function getPDO()
{
if ($this->pdo === null) {
$this->pdo = new PDO($this->dbDsn, $this->dbUser, $this->dbPass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
return $this->pdo;
}
... lines 74 - 82
}
... lines 84 - 85

And this class is done!

Passing Configuration to the Class

But now, when we create ShipLoader, we need to pass arguments. In index.php, PhpStorm is angry - required parameter $dbDsn - we're missing the first argument. We could just paste our database credentials right here. But we'll probably want them somewhere central.

Open bootstrap.php and create a new $configuration array. We'll use this now as sort of a "global configuration" variable. Put the 3 database credential things here - db_dsn - then paste the string - db_user is root and db_pass is an empty string:

13 lines bootstrap.php
... lines 1 - 2
$configuration = array(
'db_dsn' => 'mysql:host=localhost;dbname=oo_battle',
'db_user' => 'root',
'db_pass' => null,
);
... lines 8 - 13

Since we're requiring this from index.php, we can just use it there: $configuration['db_dsn] is the first argument then use db_user as the second argument and db_pass to finish things off:

123 lines index.php
... line 1
require __DIR__.'/bootstrap.php';
$shipLoader = new ShipLoader(
$configuration['db_dsn'],
$configuration['db_user'],
$configuration['db_pass']
);
... lines 9 - 123

Yes! Now the app's configuration is all in one file. In index.php, we pass this stuff to ShipLoader via its __construct() method. Then ShipLoader doesn't have any hardcoded configuration. Anything that was hardcoded before was replaced by a __construct() argument and a private property.

Make sure our ships are still battling. Refresh! Still not broken!

The Big Important Rule

Here's the rule to remember: don't put configuration inside of a service class. Replace that hardcoded configuration with an argument. This allows anyone using your class to pass in whatever they want. The hardcoding is gone, and your class is more flexible.

Oh, and by the way - this little strategy is called dependency injection. Scary! It's a tough concept for a lot of people to understand. If it's not sinking in yet, don't worry. Practice makes perfect.

Leave a comment!

13
Login or Register to join the conversation
Default user avatar
Default user avatar Arturas Lapinskas | posted 5 years ago

dont really get, what dependency injection is? its when you create another class and pass the arguments to its constractor function?

1 Reply

Hey Arturas!

Basically yes. But let me give you a *longer* explanation in case it helps :).

First, you decide that you want to create another class to hold some logic/code. That itself is not dependency injection - that is just you deciding that you want to organize your code a little bit :). But, once you have this other class - e.g. ShipLoader - and that classes needs some configuration (or it needs some other object, like a database connection object), you have a problem: how can you get access to that from inside ShipLoader? You only have two options: (1) Use a "global" keyword or some static variables (both are basically the same thing) or (2) force that configuration to be passed into your object - this can be done in a few ways, but use a constructor is the most common. The second (2) option is dependency injection. It's basically where you say "instead of using global variables, I'm going to setup my code so that anything I need is passed to me". Sometimes it's hard to explain because it's - in many ways - such an underwhelming concept: it's the way you're supposed to be coding all along.

Cheers!

1 Reply
Default user avatar

So in other words it's passing values through __construct function ? And of course it's a must to pass values, otherwise class will not be accessible right ?
P.S. Sorry for terrible English.

Reply

Hey!

Exactly: dependency injection (roughly) means passing values (strings, integers, other objects) through the __construct() method. And you're correct: if you do not pass a value into a class, then you cannot access that value from inside the class (unless you use some global variables).

Technically, there is one other way to do dependency injection - but it is ultimately the same thing. This is by calling a set* function. For example, if I need to pass a dbDsn value into my class, I can create a __construct() method. Or, instead, I can create a setDbDsn($dbDsn) method, which sets the property and call this instead. In both cases, you are passing the dbDsn value into your class - these are just 2 different ways of doing it - they both have the same result. Which is better? Using __construct() is more common and a little easier in my opinion. But, it's interesting for our conversation :).

Cheers!

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

Interesting, but you forgot to mention updating the battle.php code too in this chapter.
Refresh! Still not broken! isn't true if you refresh the result page.

Reply

Hey Marc,

Thanks for this report. Actually, we only work with index.php in this chapter. So, we changed only index.php and then on video I see we refresh the index.php. For now we do not touch battle.php page, we will update it in further chapters.

Cheers!

Reply
Marc R. Avatar

My mistake !

I don't use video to follow lessons (I don't like the presenter's voice, it reminds me of automatic text readers).

Reply
Hans petter L. Avatar
Hans petter L. Avatar Hans petter L. | posted 4 years ago | edited

I’m getting the following error:
«Catchable fatal error: Object of class PDO could not be converted to string in /lib/ShipLoader.php on line 79»
`
private function getPDO()

{

	if($this->pdo === null) {
		$pdo = new PDO($this->dbDsn, $this->dbUser, $this->dbPass);
		$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
		
		$this->pdo = $pdo;
	}
	

[79] return $this->$pdo;

}

`

I’m pretty sure I’ve got everything correct, so wondering if this might be a setting on my server config.

Reply

Hey Hanspln,

On line 79 you're calling property from variable, i.e. "return $this->$pdo;" but you should call the property explicitly: "return $this->pdo;".

Cheers!

Reply
Default user avatar
Default user avatar Danny Avery | posted 5 years ago | edited

I'm kind of lost as to what I'm doing wrong. I'm now getting a "connection refused" error. Thanks in advance:

<strong>ShipLoader.php</strong>


private $dbDsn;
private $dbUser;
private $dbPass;

public function __construct($dbDsn, $dbUser, $dbPass)
{
	$this->dbDsn = $dbDsn;
	$this->dbUser = $dbUser;
	$this->dbPass = $dbPass;
}

private function getPDO()
{
	if ($this->pdo === null) {
		$pdo = new PDO($this->dbDsn, $this->dbUser, $this->dbPass);
		$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

		$this->pdo = $pdo;
	}

	return $this->pdo;
}

<strong>bootstrap.php</strong>


$configuration = array(
	'db_dsn' => 'mysql:host=localhost:8889;dbname=oo_battle',
	'db_user' => 'root',
	'db_pass' => 'root',
);

<strong>index.php</strong>


$shipLoader = new ShipLoader(
	$configuration['db_dsn'],
	$configuration['db_user'],
	$configuration['db_pass']
);
Reply

Hey Danny,

I bet your host is incorrect - you need to specify the port in different option. And better replace localhost with 127.0.0.1.
Try this: mysql:host=127.0.0.1;dbname=oo_battle;port=8889

Does it fix the problem?

Cheers!

Reply
Default user avatar
Default user avatar Danny Avery | Victor | posted 5 years ago

I apologize once again. I just needed to restart my server(i.e. MAMP). Thanks!

Reply

Hey Danny,

Easy win! ;)

Cheers!

Reply
Cat in space

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

userVoice