I'm trying to get my head around the OOP principles and coding my own classes. As a means to learn, I have decided to convert a couple of functions I have written in Wordpress to OOP classes. These functions work together in order to output the correct post links on single pages according to referrers (4 of them) set in the URL.
This is the setup with a basic workflow (The workflow can change as I go along):
4 query variables are set to the URL according to archive page, ie, one query variable for taxonomy pages, one query variable set for the author pages and so one. No page can ever have more than one custom query variable. This 4 variables is retrieved by my first class and checked against a given global variable, in this case $_GET
. I have not hardcoded the 4 variables in my class, and this goes for $_GET
as well to keep the class testable. If the value exists in the URL, the key/value pair is returned through the has*
methods. These methods return null
if no match is found. (this is raw data which will be sanitized/escaped by the classes that will use this data)
Here is the full class
<?php
namespace PG\Single\Post\Navigation;
/**
* Test set values against the super global given. Returns conditional properties
* which is boolean values. true is returned on success and false on failure.
*
* @param $superGlobalVar Super global to test the values against
* @param (string) $authorReferrer
* @param (string) $dateReferrer
* @param (string) $searchReferrer
* @param (string) $taxReferrer
*/
class RequestReferrerHandler implements RequestReferrerHandlerInterface
{
/**
* @since 1.0.0
* @access protected
* @var (array) $superGlobalVar
*/
protected $superGlobalVar;
/**
* @since 1.0.0
* @access protected
* @var (string) $authorReferrer
*/
protected $authorReferrer;
/**
* @since 1.0.0
* @access protected
* @var (string) $dateReferrer
*/
protected $dateReferrer;
/**
* @since 1.0.0
* @access protected
* @var (string) $searchReferrer
*/
protected $searchReferrer;
/**
* @since 1.0.0
* @access protected
* @var (string) $taxReferrer
*/
protected $taxReferrer;
/**
* Public constructor method.
*
* @param $superGlobalVar Super global to get data from
* @param $authorReferrer Query variable from author referrer to test
* @param $dateReferrer Query variable from date referrer to test
* @param $searchReferrer Query variable from search referrer to test
* @param $taxReferrer Query variable from taxonomy referrer to test
*/
public function __construct($superGlobalVar = null, $authorReferrer= null, $dateReferrer = null, $searchReferrer = null, $taxReferrer = null)
{
$this->superGlobalVar = $superGlobalVar;
$this->authorReferrer = $authorReferrer;
$this->dateReferrer = $dateReferrer;
$this->searchReferrer = $searchReferrer;
$this->taxReferrer = $taxReferrer;
}
/**
* Setter setSuperGlobalVar.
*
* @since 1.0.0
* @param $superGlobalVar
* @return $this
*/
public function setSuperGlobalVar($superGlobalVar)
{
$this->superGlobalVar = $superGlobalVar;
return $this;
}
/**
* Returns an array of super global variables.
*
* @since 1.0.0
* @return (array) $this->superGlobalVar
*/
public function getSuperGlobalVar()
{
return $this->superGlobalVar;
}
/**
* Setter setAuthorReferrer
*
* @since 1.0.0
* @param $authorReferrer
* @return $this
*/
public function setAuthorReferrer($authorReferrer)
{
$this->authorReferrer = $authorReferrer;
return $this;
}
/**
* Returns the value of the $authorReferrer property.
*
* @since 1.0.0
* @return (array) $this->authorReferrer
*/
public function getAuthorReferrer()
{
return $this->authorReferrer;
}
/**
* Setter setDateReferrer.
*
* @since 1.0.0
* @param $dateReferrer
* @return $this
*/
public function setDateReferrer($dateReferrer)
{
$this->dateReferrer = $dateReferrer;
return $this;
}
/**
* Returns the value of the $dateReferrer property.
*
* @since 1.0.0
* @return (array) $this->dateReferrer
*/
public function getDateReferrer()
{
return $this->dateReferrer;
}
/**
* Setter setSearchReferrer.
*
* @since 1.0.0
* @param $searchReferrer
* @return $this
*/
public function setSearchReferrer($searchReferrer)
{
$this->searchReferrer = $searchReferrer;
return $this;
}
/**
* Returns the value of the $searchReferrer property.
*
* @since 1.0.0
* @return (array) $this->searchReferrer
*/
public function getSearchReferrer()
{
return $this->searchReferrer;
}
/**
* Setter setTaxReferrer.
*
* @since 1.0.0
* @param $taxReferrer
* @return $this
*/
public function setTaxReferrer($taxReferrer)
{
$this->taxReferrer = $taxReferrer;
return $this;
}
/**
* Returns the value of the $taxReferrer property.
*
* @since 1.0.0
* @return (array) $this->taxReferrer
*/
public function getTaxReferrer()
{
return $this->$taxReferrer;
}
/**
* Test $authorReferrer against $superGlobalVar.
*
* @since 1.0.0
* @return (bool) true on success or false on failure
*/
public function isAuthorReferrer()
{
if ($this->authorReferrer && isset($this->superGlobalVar[$this->authorReferrer])) {
$isAuthorReferrer = true;
} else {
$isAuthorReferrer = false;
}
return $isAuthorReferrer;
}
/**
* Test $authorReferrer against $superGlobalVar
*
* @since 1.0.0
* @return (bool) true on success or false on failure
*/
public function isDateReferrer()
{
if ($this->dateReferrer && isset($this->superGlobalVar[$this->dateReferrer])) {
$isDateReferrer = true;
} else {
$isDateReferrer = false;
}
return $isDateReferrer;
}
/**
* Test $authorReferrer against $superGlobalVar.
*
* @since 1.0.0
* @return (bool) true on success or false on failure
*/
public function isSearchReferrer()
{
if ($this->searchReferrer && isset($this->superGlobalVar[$this->searchReferrer])) {
$isSearchReferrer = true;
} else {
$isSearchReferrer = false;
}
return $isSearchReferrer;
}
/**
* Test $authorReferrer against $superGlobalVar.
*
* @since 1.0.0
* @return (bool) true on success or false on failure
*/
public function isTaxReferrer()
{
if ($this->taxReferrer && isset($this->superGlobalVar[$this->taxReferrer])) {
$isTaxReferrer = true;
} else {
$isTaxReferrer = false;
}
return $isTaxReferrer;
}
/**
* Conditional which check if the current post is a referred post.
*
* @since 1.0.0
* @return (bool) true on success or false on failure
*/
public function isReferredPost()
{
if ($this->isAuthorReferrer() || $this->isDateReferrer() || $this->isSearchReferrer() || $this->isTaxReferrer()) {
$isReferredPost = true;
} else {
$isReferredPost = false;
}
return $isReferredPost;
}
/**
* Return the value from the super global when the current post is a post referred from
* an author archive page.
*
* @since 1.0.0
* @return (array) $authorReferrerValue
*/
public function hasAuthorReferrerValue()
{
if ($this->isAuthorReferrer()) {
$authorReferrerValue = [$this->authorReferrer => $this->superGlobalVar[$this->authorReferrer]];
} else {
$authorReferrerValue = null;
}
return $authorReferrerValue;
}
/**
* Return the value from the super global when the current post is a post referred from
* a date archive page.
*
* @since 1.0.0
* @return (array) $dateReferrerValue
*/
public function hasDateReferrerValue()
{
if ($this->isDateReferrer()) {
$dateReferrerValue = [$this->dateReferrer => $this->superGlobalVar[$this->dateReferrer]];
} else {
$dateReferrerValue = null;
}
return $dateReferrerValue;
}
/**
* Return the value from the super global when the current post is a post referred from
* a search page.
*
* @since 1.0.0
* @return (array) $searchReferrerValue
*/
public function hasSearchReferrerValue()
{
if ($this->isSearchReferrer()) {
$searchReferrerValue = [$this->searchReferrer => $this->superGlobalVar[$this->searchReferrer]];
} else {
$searchReferrerValue = null;
}
return $searchReferrerValue;
}
/**
* Return the value from the super global when the current post is a post referred from
* a taxonomy archive page.
*
* @since 1.0.0
* @return (array) $taxReferrerValue
*/
public function hasTaxReferrerValue()
{
if ($this->isTaxReferrer()) {
$taxReferrerValue = [$this->taxReferrer => $this->superGlobalVar[$this->taxReferrer]];
} else {
$taxReferrerValue = null;
}
return $taxReferrerValue;
}
}
This is how I use this class
$b = new RequestReferrerHandler($_GET, 'aq', 'dq', 'sq', 'tq');
?><pre><?php var_dump($b->hasAuthorReferrerValue()); ?></pre><?php
?><pre><?php var_dump($b->hasDateReferrerValue()); ?></pre><?php
?><pre><?php var_dump($b->hasSearchReferrerValue()); ?></pre><?php
?><pre><?php var_dump($b->hasTaxReferrerValue()); ?></pre><?php
For testing purposes you can inject something like ['aq' => '1']
into the class instead of $_GET
This is where I'm stuck now and have no idea how to move on. I need to construct two classes which will both use the same methods from the class above, one class that will construct query arguments from the has*
methods from the above class, and one class will create query_vars
also from the has*
methods from the above class that will be used to construct new post links
So, in short, both classes will make use of the exact same for methods from the above class
hasAuthorReferrerValue();
hasDateReferrerValue();
hasSearchReferrerValue();
hasTaxReferrerValue();
Just as an example, here is an example of how the two classes should look like. (I have omitted some of the methods here to make the code more manageable)
ClassA
<?php
namespace PG\Single\Post\Navigation;
class ClassA //Just a generic name for testing purposes. Will also implement ClassAInterface
{
protected $handler;
public function __construct(RequestReferrerHandlerInterface $handler)
{
$this->handler = $handler;
}
public function santizeAuthor()
{
$author = $this->handler->hasAuthorReferrerValue(); // Value will be either null or single key/value pair array. Example ['aq' => '1']
if ($author) {
$author = array_values($author);
$author = ['author' => (int)htmlspecialchars($author[0])]; //Will output ['author' => 1]
}
return $author; //Returns null or the array ['author' => 1]
}
public function santizeDate()
{
$date = $this->handler->hasDateReferrerValue();
if ($date) {
// @TODO Still to work out
}
return $date;
}
//etc
public function queryArguments() // Will be used in the controller class ClassC
{
$queryArgs = null;
if ($this->santizeAuthor()) {
$queryArgs = $this->santizeAuthor();
} elseif ($this->santizeDate) {
$queryArgs = $this->santizeDate();
} // etc
return $queryArgs; //Will return null if all 4 conditions fail or return the value from the one that returns true
}
}
ClassB
<?php
namespace PG\Single\Post\Navigation;
class ClassB //Just a generic name for testing purposes. Will also implement ClassBInterface
{
protected $handler;
public function __construct(RequestReferrerHandlerInterface $handler)
{
$this->handler = $handler;
}
public function santizeAuthor()
{
$author = $this->handler->hasAuthorReferrerValue(); // Value will be either null or single key/value pair array. Example ['aq' => '1']
if ($author) {
foreach ($author as $k=>$v)
$author[htmlspecialchars($k)] = (int)htmlspecialchars($v);
}
return $author; //Returns null or the array ['aq' => 1]
}
public function santizeDate()
{
$date = $this->handler->hasDateReferrerValue();
if ($date) {
// @TODO Still to work out
}
return $date;
}
//etc
public function queryVars() // Will be used in the controller class ClassC
{
$queryVars = null;
if ($this->santizeAuthor()) {
$queryVars = $this->santizeAuthor();
} elseif ($this->santizeDate) {
$queryVars = $this->santizeDate();
} // etc
return $queryVars; //Will return null if all 4 conditions fail or return the value from the one that returns true
}
}
The queryArguments()
method from ClassA
and the queryVars()
method from ClassB
will be used in other classes (or one controller class)
My total lack of proper knowledge coming into OOP, confusion with separation of concerns, encapsulation, SOLID principles and keeping class testable have me second guessing my code, and I do feel I am missing something.
Is there anyway I can optimize the above. I am not asking for any type of code rewritting, all I need is proper pointers and ideas on optimizing this to bring it up to standard if it is not. It would be a real plus if anyone can give code samples, something like an outline skeleton
Looking over your code you are definitely off to a good start. You are already using one good rule of thumb when programming in OOP - program to an interface, not an implementation. By the term interface I'm not referring only to actual interfaces, but abstract classes as well.
So at the heart of your question you want to have two classes, ClassA
and ClassB
that both use common methods from RequestReferrerHandler
. You already have the ground work laid out to do that with your interface RequestReferrerHandlerInterface
. So we'll say that you have an interface that looks like this:
interface RequestReferrerHandlerInterface
{
public function hasAuthorReferrerValue();
public function hasDateReferrerValue();
public function hasSearchReferrerValue();
public function hasTaxReferrerValue();
}
So long as this interface is implemented by the RequestReferrerHandler
you can type hint the interface as the constructor requirements for ClassA
and ClassB
. But this isn't anything new because you were already doing this.
There are two things in particular that stand out at me as potential sore thumbs. First, since you want the responsibilities of your classes to be small, you should take the responsibility of providing data to the RequestReferrerHandler
away from itself and give it to your Controller
. In other words, do not inject $_GET
into your class. Make sure your Controller
has all the information it needs to properly create the RequestReferrerHandler
Let's take a look at your RequestReferrerHandler
class, flushed with all of the methods it will need.
class RequestReferrerHandler implements RequestReferrerHandlerInterface
{
private $author;
private $date;
private $search;
private $tax;
public function __construct($author = null, $date = null, $search = null, $tax = null)
{
$this->setAuthorReferrer($author);
$this->setDateReferrer($date);
$this->setSearchReferrer($search);
$this->setTaxReferrer($tax);
}
public function hasAuthorReferrerValue()
{
return $this->author !== null ? true : false;
}
public function hasDateReferrerValue()
{
return $this->date !== null ? true : false;
}
public function hasSearchReferrerValue()
{
return $this->search !== null ? true : false;
}
public function hasTaxReferrerValue()
{
return $this->tax !== null ? true : false;
}
public function getAuthorReferrer()
{
return $this->author;
}
public function getDateReferrer()
{
return $this->date;
}
public function getSearchReferrer()
{
return $this->search;
}
public function getTaxReferrer()
{
return $this->tax;
}
public function setAuthorReferrer($author)
{
$this->author = $author;
}
public function setDateReferrer($date)
{
$this->date = $date;
}
public function setSearchReferrer($search)
{
$this->search = $search;
}
public function setTaxReferrer($tax)
{
$this->tax = $tax;
}
}
The second thing that sticks out is the santize()
methods. Do you see how they are duplicated in both ClassA
and ClassB
? The sanitizeAuthor()
is different among the two classes, but how about the rest? This is a case where the DRY (Don't Repeat Yourself) principle can help out. Since multiple classes may have to sanitize data in similar ways it make sense to abstract that away from your classes.
Let's take a look at how to do that and then we will come back to your concrete classes. First create a new interface that will specify methods that must be exposed by an object that can sanitize data.
interface SanitizerInterface
{
public function sanitizeAuthor();
public function sanitizeDate();
public function sanitizeSearch();
public function sanitizeTaxonomy();
}
Now, if every object you had of ClassX
implemented these four methods in different ways you could start implementing it in different classes that simply sanitize data. However, for this example we will say that is not the case. Let's make the assumption that sanitizeAuthor()
could be different among ClassA
and ClassB
(which it is in your code) and all the other methods will be implemented exactly the same. This is case where we can use an abstract class that will implement the sanitizer methods.
abstract class AbstractSanitizer implements SanitizerInterface
{
protected $handler;
public function __construct() {}
public function setHandler(RequestReferrerHandlerInterface $handler)
{
$this->handler = $handler;
}
/* For this example we are saying that sanitizeDate(), sanitizeTaxonomy() and
* sanitizeSearch() will be the same no matter what. So let's implement them
* and leave the child classes to implement sanitizeAuthor().
*
* Implement the details of the sanitizer function to fit your needs.
*/
public function sanitizeDate()
{
if($this->handler !== null)
{
//Perform whatever tasks to sanitize the date
$sanitized = strtoupper($this->handler->getDateReferrer());
echo "Sanitize date -> switch to uppercase letters.\n";
$this->handler->setDateReferrer($sanitized);
}
}
public function sanitizeSearch()
{
if($this->handler !== null)
{
//Perform whatever tasks to sanitize the search
$sanitized = strtolower($this->handler->getSearchReferrer());
echo "Sanitize search -> switch to lowercase letters.\n";
$this->handler->setSearchReferrer($sanitized);
}
}
public function sanitizeTaxonomy()
{
if($this->handler !== null)
{
//Perform whatever tasks to sanitize the taxonomy
$sanitized = str_replace(" ", "_", $this->handler->getTaxReferrer());
echo "Sanitize Taxonomy -> convert spaces to underscores.\n";
$this->handler->setTaxReferrer($sanitized);
}
}
}
Some things to take note of right off the bat. First, you'll notice there is the setHandler()
method which accepts an instance of the RequestReferrerHandlerInterface
. Why is this there? Convenience for the most part. Since we have taken the sanitizing behavior and encapsulated it into its own class it would be nice it we gave the sanitizer a way to update the concrete RequestReferrerHandler
it is using with the updated output from a sanitize method.
Next thing, we are using methods from the RequestReferrerHandler
class that are not specified in the RequestReferrerHandlerInterface
. This isn't an immediate problem per se, because we know that methods like the getters and setters are in the class. However, type hinting to the interface alone doesn't guarantee that those methods will be available if you ever decided to implement that interface with a different concrete object. Therefore, we need to update the RequestReferrerHandlerInterface
with methods that will guarantee their availability.
interface RequestReferrerHandlerInterface
{
public function hasAuthorReferrerValue();
public function hasDateReferrerValue();
public function hasSearchReferrerValue();
public function hasTaxReferrerValue();
public function getAuthorReferrer();
public function getDateReferrer();
public function getSearchReferrer();
public function getTaxReferrer();
public function setAuthorReferrer($author);
public function setDateReferrer($date);
public function setSearchReferrer($search);
public function setTaxReferrer($tax);
}
Now, back to those sanitizers. We know that ClassA
and ClassB
will implement their sanitizeAuthor()
methods differently. The abstract class AbstractSanitizer
was made the way it was because the sanitizeAuthor()
method from the the SanitizerInteface
isn't implemented in AbstractSanitizer
so we have to extend it to provide the functionality. We will need the following two classes to do this:
class SanitizerForClassA extends AbstractSanitizer
{
/* This class must provide an implementation for how ClassA will
* handle the sanitizeAuthor() method.
*/
public function sanitizeAuthor()
{
if($this->handler !== null)
{
//Perform whatever tasks to sanitize the for ClassA
$sanitized = array("author" => $this->handler->getAuthorReferrer());
echo "Sanitize author -> ClassA makes author an array.\n";
$this->handler->setAuthorReferrer($sanitized);
}
}
}
class SanitizerForClassB extends AbstractSanitizer
{
/* This class must provide an implementation for how ClassB will
* handle the sanitizeAuthor() method.
*/
public function sanitizeAuthor()
{
if($this->handler !== null)
{
//Perform whatever tasks to sanitize the for ClassB
$sanitized = new stdClass();
$sanitized->author = $this->handler->getAuthorReferrer();
echo "Sanitize author -> ClassB makes author an object property. \n";
$this->handler->setAuthorReferrer($sanitized);
}
}
}
These two concrete classes can be used with ClassA
and ClassB
to sanitize data within the concrete RequestReferrerHandler
methods that will be passed into them.
So moving on, let's look at the spec for ClassA
and ClassB
. We know that ClassA
will need the method queryArguments()
, ClassB
will need the method queryVars()
and both classes will need to allow and instance of a RequestReferrerHandlerInterface
and SanitizerInterface
in their constructors. We will handle the constructor requirement with one interface, then two other interfaces will extend that to provide all the method requirements needed for ClassA
and ClassB
.
interface SanitizableHandlerInterface
{
public function __construct(RequestReferrerHandlerInterface $handler, SanitizerInterface $sanitizer);
}
interface QueryVarsInterface extends SanitizableHandlerInterface
{
public function queryVars();
}
interface QueryArgumentsInterface extends SanitizableHandlerInterface
{
public function queryArguments();
}
Since we are now getting down to it, let's take a look at those classes that will use these.
class ClassA implements QueryArgumentsInterface
{
private $handler;
private $sanitizer;
public function __construct(RequestReferrerHandlerInterface $handler, SanitizerInterface $sanitizer)
{
$this->handler = $handler;
$this->sanitizer = $sanitizer;
$this->sanitizer->setHandler($this->handler);
}
public function queryArguments() // Will be used in the controller class ClassC
{
$queryArgs = null;
if($this->handler->hasAuthorReferrerValue())
{
$this->sanitizer->sanitizeAuthor();
$queryArgs = $this->handler->getAuthorReferrer();
}
if($this->handler->hasDateReferrerValue())
{
$this->sanitizer->sanitizeDate();
$queryArgs = $this->handler->getDateReferrer();
}
if($this->handler->hasSearchReferrerValue())
{
$this->sanitizer->sanitizeSearch();
$queryArgs = $this->handler->getSearchReferrer();
}
if($this->handler->hasTaxReferrerValue())
{
$this->sanitizer->sanitizeTaxonomy();
$queryArgs = $this->handler->getTaxReferrer();
}
return $queryArgs; //Will return null if all 4 conditions fail or return the value from the one that returns true
}
}
class ClassB implements QueryVarsInterface
{
private $handler;
private $sanitizer;
public function __construct(RequestReferrerHandlerInterface $handler, SanitizerInterface $sanitizer)
{
$this->handler = $handler;
$this->sanitizer = $sanitizer;
$this->sanitizer->setHandler($this->handler);
}
public function queryVars() // Will be used in the controller class ClassC
{
$queryVars = null;
if($this->handler->hasAuthorReferrerValue())
{
$this->sanitizer->sanitizeAuthor();
$queryVars = $this->handler->getAuthorReferrer();
}
if($this->handler->hasDateReferrerValue())
{
$this->sanitizer->sanitizeDate();
$queryVars = $this->handler->getDateReferrer();
}
if($this->handler->hasSearchReferrerValue())
{
$this->sanitizer->sanitizeSearch();
$queryVars = $this->handler->getSearchReferrer();
}
if($this->handler->hasTaxReferrerValue())
{
$this->sanitizer->sanitizeTaxonomy();
$queryVars = $this->handler->getTaxReferrer();
}
return $queryVars; //Will return null if all 4 conditions fail or return the value from the one that returns true
}
}
There you have it, the ground work is built. You'll notice that in the constructors properties are set for the handler and sanitizer class that was given and then the sanitizer is given the reference to the handler. (Remember, the sanitizers have a reference to the handler so that sanitized properties in the handler are automatically updated. The individual classes don't need to worry about that now.)
So now the million dollar question is how to use this. Well, you need a controller that can accept ClassA
and ClassB
. We will type hint these by their respective interfaces as well.
class Controller
{
public function __construct() {}
public function doStuff(QueryArgumentsInterface $argsClass, QueryVarsInterface $varsClass)
{
var_dump($argsClass->queryArguments());
var_dump($varsClass->queryVars());
}
}
In your version of queryArguments()
and queryVars()
you expected sanitized data for a return value. Let's plug some data in and see what we get. (Note: As you already figured out none of the sanitize methods I used are doing what you were doing, they are illustrative only.)
//TEST DRIVE
//Create a controller that will use the classes
$controller = new Controller();
//Now make use of your new shiny handlers and sanitizers
$controller->doStuff(
new ClassA(new RequestReferrerHandler("Mark Twain", null, null, null), new SanitizerForClassA()),
new ClassB(new RequestReferrerHandler(null, "January 1st, 1999", null, null), new SanitizerForClassB())
);
$controller->doStuff(
new ClassA(new RequestReferrerHandler(null, null, "OK Google Now!", null), new SanitizerForClassA()),
new ClassB(new RequestReferrerHandler(null, null, null, "Super Awesome Taxonomy Tables"), new SanitizerForClassB())
);
$controller->doStuff(
new ClassA(new RequestReferrerHandler(null, "January 1st, 1999", null, null), new SanitizerForClassA()),
new ClassB(new RequestReferrerHandler("Mark Twain", null, null, null), new SanitizerForClassB())
);
$controller->doStuff(
new ClassA(new RequestReferrerHandler(null, null, null, "Super Awesome Taxonomy Tables"), new SanitizerForClassA()),
new ClassB(new RequestReferrerHandler(null, null, "OK Google Now!", null), new SanitizerForClassB())
);
Here's the output:
Sanitize author -> ClassA makes author an array.
array (size=1)
'author' => string 'Mark Twain' (length=10)
Sanitize date -> switch to uppercase letters.
string 'JANUARY 1ST, 1999' (length=17)
Sanitize search -> switch to lowercase letters.
string 'ok google now!' (length=14)
Sanitize Taxonomy -> convert spaces to underscores.
string 'Super_Awesome_Taxonomy_Tables' (length=29)
Sanitize date -> switch to uppercase letters.
string 'JANUARY 1ST, 1999' (length=17)
Sanitize author -> ClassB makes author an object property.
object(stdClass)[15]
public 'author' => string 'Mark Twain' (length=10)
Sanitize Taxonomy -> convert spaces to underscores.
string 'Super_Awesome_Taxonomy_Tables' (length=29)
Sanitize search -> switch to lowercase letters.
string 'ok google now!' (length=14)
So what did all of this cost you? Short answer - complexity. It took 4 interfaces, 1 abstract class and a handful of concrete classes to output a little bit of data to the screen.
What do you gain? Short answer - flexibility. In the future you may wish to add more classes that implement either the QueryVarsInterface
or QueryArgumentsInterface
. Consider these classes ClassC
, ClassD
and ClassE
. All of these classes will need a sanitizer class to go with them (that is if SanitizerForClassA
or SanitizerForClassB
do not fit the bill) and it would be tedious to keep having to write sanitizer classes. Well, good thing for you, since you were programming to an interface all along, you won't have that problem. You can easily make a GenericSanitizer
with a default implementation of the sanitizeAuthor()
method. Use can use this class with Controller::doStuff()
in any case where you don't need a specialized sanitizer class. You could just as easily implement different concrete classes of QueryArgumentInterface
or QueryVarsInterface
to test out experimental features you want to add without tampering with your current classes.
Hopefully this has given you some insight on some OOP principles. Here is a complete copy of all of the code above. Slap this in an empty PHP file and run it to see everything in action. Happy programming!
<?php
/*
* INTERFACES
*/
interface RequestReferrerHandlerInterface
{
public function hasAuthorReferrerValue();
public function hasDateReferrerValue();
public function hasSearchReferrerValue();
public function hasTaxReferrerValue();
public function getAuthorReferrer();
public function getDateReferrer();
public function getSearchReferrer();
public function getTaxReferrer();
public function setAuthorReferrer($author);
public function setDateReferrer($date);
public function setSearchReferrer($search);
public function setTaxReferrer($tax);
}
interface SanitizerInterface
{
public function sanitizeAuthor();
public function sanitizeDate();
public function sanitizeSearch();
public function sanitizeTaxonomy();
}
interface SanitizableHandlerInterface
{
public function __construct(RequestReferrerHandlerInterface $handler, SanitizerInterface $sanitizer);
}
interface QueryVarsInterface extends SanitizableHandlerInterface
{
public function queryVars();
}
interface QueryArgumentsInterface extends SanitizableHandlerInterface
{
public function queryArguments();
}
/*
* ABSTRACT CLASSES
*/
abstract class AbstractSanitizer implements SanitizerInterface
{
protected $handler;
public function __construct() {}
public function setHandler(RequestReferrerHandlerInterface $handler)
{
$this->handler = $handler;
}
/* For this example we are saying that sanitizeDate(), sanitizeTaxonomy() and
* sanitizeSearch() will be the same no matter what. So let's implement them
* and leave the child classes to implement sanitizeAuthor().
*
* Implement the details of the sanitizer function to fit your needs.
*/
public function sanitizeDate()
{
if($this->handler !== null)
{
//Perform whatever tasks to sanitize the date
$sanitized = strtoupper($this->handler->getDateReferrer());
echo "Sanitize date -> switch to uppercase letters.\n";
$this->handler->setDateReferrer($sanitized);
}
}
public function sanitizeSearch()
{
if($this->handler !== null)
{
//Perform whatever tasks to sanitize the search
$sanitized = strtolower($this->handler->getSearchReferrer());
echo "Sanitize search -> switch to lowercase letters.\n";
$this->handler->setSearchReferrer($sanitized);
}
}
public function sanitizeTaxonomy()
{
if($this->handler !== null)
{
//Perform whatever tasks to sanitize the taxonomy
$sanitized = str_replace(" ", "_", $this->handler->getTaxReferrer());
echo "Sanitize Taxonomy -> convert spaces to underscores.\n";
$this->handler->setTaxReferrer($sanitized);
}
}
}
/*
* CONCRETE CLASSES
*/
class RequestReferrerHandler implements RequestReferrerHandlerInterface
{
private $author;
private $date;
private $search;
private $tax;
public function __construct($author = null, $date = null, $search = null, $tax = null)
{
$this->setAuthorReferrer($author);
$this->setDateReferrer($date);
$this->setSearchReferrer($search);
$this->setTaxReferrer($tax);
}
public function hasAuthorReferrerValue()
{
return $this->author !== null ? true : false;
}
public function hasDateReferrerValue()
{
return $this->date !== null ? true : false;
}
public function hasSearchReferrerValue()
{
return $this->search !== null ? true : false;
}
public function hasTaxReferrerValue()
{
return $this->tax !== null ? true : false;
}
public function getAuthorReferrer()
{
return $this->author;
}
public function getDateReferrer()
{
return $this->date;
}
public function getSearchReferrer()
{
return $this->search;
}
public function getTaxReferrer()
{
return $this->tax;
}
public function setAuthorReferrer($author)
{
$this->author = $author;
}
public function setDateReferrer($date)
{
$this->date = $date;
}
public function setSearchReferrer($search)
{
$this->search = $search;
}
public function setTaxReferrer($tax)
{
$this->tax = $tax;
}
}
class SanitizerForClassA extends AbstractSanitizer
{
/* This class must provide an implementation for how ClassA will
* handle the sanitizeAuthor() method.
*/
public function sanitizeAuthor()
{
if($this->handler !== null)
{
//Perform whatever tasks to sanitize the for ClassA
$sanitized = array("author" => $this->handler->getAuthorReferrer());
echo "Sanitize author -> ClassA makes author an array.\n";
$this->handler->setAuthorReferrer($sanitized);
}
}
}
class SanitizerForClassB extends AbstractSanitizer
{
/* This class must provide an implementation for how ClassB will
* handle the sanitizeAuthor() method.
*/
public function sanitizeAuthor()
{
if($this->handler !== null)
{
//Perform whatever tasks to sanitize the for ClassB
$sanitized = new stdClass();
$sanitized->author = $this->handler->getAuthorReferrer();
echo "Sanitize author -> ClassB makes author an object property. \n";
$this->handler->setAuthorReferrer($sanitized);
}
}
}
class ClassA implements QueryArgumentsInterface
{
private $handler;
private $sanitizer;
public function __construct(RequestReferrerHandlerInterface $handler, SanitizerInterface $sanitizer)
{
$this->handler = $handler;
$this->sanitizer = $sanitizer;
$this->sanitizer->setHandler($this->handler);
}
public function queryArguments() // Will be used in the controller class ClassC
{
$queryArgs = null;
if($this->handler->hasAuthorReferrerValue())
{
$this->sanitizer->sanitizeAuthor();
$queryArgs = $this->handler->getAuthorReferrer();
}
if($this->handler->hasDateReferrerValue())
{
$this->sanitizer->sanitizeDate();
$queryArgs = $this->handler->getDateReferrer();
}
if($this->handler->hasSearchReferrerValue())
{
$this->sanitizer->sanitizeSearch();
$queryArgs = $this->handler->getSearchReferrer();
}
if($this->handler->hasTaxReferrerValue())
{
$this->sanitizer->sanitizeTaxonomy();
$queryArgs = $this->handler->getTaxReferrer();
}
return $queryArgs; //Will return null if all 4 conditions fail or return the value from the one that returns true
}
}
class ClassB implements QueryVarsInterface
{
private $handler;
private $sanitizer;
public function __construct(RequestReferrerHandlerInterface $handler, SanitizerInterface $sanitizer)
{
$this->handler = $handler;
$this->sanitizer = $sanitizer;
$this->sanitizer->setHandler($this->handler);
}
public function queryVars() // Will be used in the controller class ClassC
{
$queryVars = null;
if($this->handler->hasAuthorReferrerValue())
{
$this->sanitizer->sanitizeAuthor();
$queryVars = $this->handler->getAuthorReferrer();
}
if($this->handler->hasDateReferrerValue())
{
$this->sanitizer->sanitizeDate();
$queryVars = $this->handler->getDateReferrer();
}
if($this->handler->hasSearchReferrerValue())
{
$this->sanitizer->sanitizeSearch();
$queryVars = $this->handler->getSearchReferrer();
}
if($this->handler->hasTaxReferrerValue())
{
$this->sanitizer->sanitizeTaxonomy();
$queryVars = $this->handler->getTaxReferrer();
}
return $queryVars; //Will return null if all 4 conditions fail or return the value from the one that returns true
}
}
class Controller
{
public function __construct() {}
public function doStuff(QueryArgumentsInterface $argsClass, QueryVarsInterface $varsClass)
{
var_dump($argsClass->queryArguments());
var_dump($varsClass->queryVars());
}
}
/*
* TEST DRIVE
*/
//Create a controller that will use the classes
$controller = new Controller();
//Now make use of your new shiny handlers and sanitizers
$controller->doStuff(
new ClassA(new RequestReferrerHandler("Mark Twain", null, null, null), new SanitizerForClassA()),
new ClassB(new RequestReferrerHandler(null, "January 1st, 1999", null, null), new SanitizerForClassB())
);
$controller->doStuff(
new ClassA(new RequestReferrerHandler(null, null, "OK Google Now!", null), new SanitizerForClassA()),
new ClassB(new RequestReferrerHandler(null, null, null, "Super Awesome Taxonomy Tables"), new SanitizerForClassB())
);
$controller->doStuff(
new ClassA(new RequestReferrerHandler(null, "January 1st, 1999", null, null), new SanitizerForClassA()),
new ClassB(new RequestReferrerHandler("Mark Twain", null, null, null), new SanitizerForClassB())
);
$controller->doStuff(
new ClassA(new RequestReferrerHandler(null, null, null, "Super Awesome Taxonomy Tables"), new SanitizerForClassA()),
new ClassB(new RequestReferrerHandler(null, null, "OK Google Now!", null), new SanitizerForClassB())
);
As I can see in your previous questions, you look for a way to rationalize your OOP development. This is why I won't give you a fish but I will help you to fish by yourself. This means that I'm gonna (try to) give the base that you should know to do a strong OOP code.
1. SRP and composition
As I can see in your questions, you try to separate responsability of your classes. This is a good thing in OOP of course. What you do is called Single Responsability Principle (SRP). This principle imply that you prefer composition over inheritance.
// Composition
class Car implements VehicleInterface
{
private $motor;
}
class Motor implements MotorInterface
In this case, the car and the motor have 2 different responsabilities.
// Inheritance
class Car extends MotorVehicle
{
}
In the case of inheritance, you make a high compling between the vehicle and motor notions.
Imagine I want to had a new notion like the movement for example:
// Composition
class Car implements VehicleInterface
{
private $motor;
private $movement;
}
class Motor implements MotorInterface
class Drive implements MovementInterface
No problem with composition.
// Inheritance
class Car extends MotorVehicle, DriveVehicle
{
}
Multiple inheritance is bad (and even not possible in PHP) because you break the SRP. Inheritance should only be used for factoring code for classes of the same responsability. As you should have only one responsability by class, you shouldn't use multiple inheritance. Other possibilities are bad because you cannot tell me that a Car is more a MotorVehicle
than a DriveVehicle
.
In your case, you have some request referrer handlers and some sanitizer.
2. Interfaces and low coupling
As I told you in my previous answer your do right when using interfaces to make a low coupling between your classes. This give you a more maintenable, scalable and testable code. Take the previous example:
class Car implements VehicleInterface
{
private $motor;
}
class PetrolMotor implements MotorInterface
class DieselMotor implements MotorInterface
Your car can easily take different kind of motors now!
The idea that should drive your mind here, is that a class should never use another class directly but an interface describing a behaviour.
In your case, class A
and class B
should implement an interface SanitizerInterface
.
3. Dependency Injection
At this point, you want to set your handlers in your sanitizer. The better way is to use dependency injection. This is really simple!
class Car implements VehicleInterface
{
private $motor;
public function __construct(MotorInterface $motor)
{
$this->motor = $motor;
}
}
class PetrolMotor implements MotorInterface
{
}
class DieselMotor implements MotorInterface
{
}
$motor = new PetrolMotor();
$car = new Car($motor);
In your case, you have to inject the request referrer handler in your sanitizer.
4. SOA
Service Oriented Architecture (SOA) applied to OOP is a great way to rationalize your code.
// Standard OOP
$car = new Car();
$buyer = new Person();
$store = new Store();
// Many solutions to buy a car!
$buyer->buy($car, $store);
// or
$store->sell($car, $buyer);
// or
// ...
In standard OOP, you are often confronted to duplicated code because of that. Where should I code this method? Where I can now where I (or someone else) already coded this method? This problem was so boring for me before! I was not able to rationalize my development.
// SOA
$car = new Car();
$buyer = new Person();
$store = new Store();
$saleHandler = new SaleHandler();
$saleHandler->sell($car, $buyer, $store);
In SOA, you have a "service class" (here SaleHandler
) (basically implemented as a singleton) which handle the maniplation of "data classes" (here Car
, Person
and Store
). There is no intelligence in your data classes (you often only have getters and setters on properties). This way, you know where is your code for sales!
In your case, it seems that your request referrer handlers and sanitizers are some kind of services, so it's ok.
Conclusion
In conclusion, you use intuitively some really good OOP practices. Now, you can apply them and know why!
However, I would highly advice you to try a framework like Symfony2. It will provide you with a strong base for you PHP development and a really nice dependency injection component allowing you to define all the dependencies of your classes in configuration files to have a real dynamical code. It will also help you to do SOA with its services.
Using a framework is a good thing to power up your developments and for your professional life (a developer knowing a framework is a lot more sought after). As PHP frameworks are mainly opensource, you can also participate and give you a nice visibility for recruiters.