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
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.
In this case, the car and the motor have 2 different responsabilities.
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:
No problem with composition.
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 aDriveVehicle
.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:
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.
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!
4. SOA
Service Oriented Architecture (SOA) applied to OOP is a great way to rationalize your code.
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.
In SOA, you have a "service class" (here
SaleHandler
) (basically implemented as a singleton) which handle the maniplation of "data classes" (hereCar
,Person
andStore
). 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!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.
If you want to use the same object of
RequestReferrerHandler
class to ClassA and ClassB, then your strategy is correct. Just need to use the object ofRequestReferrerHandler
class to instantiate ClassA and ClassB. Then you can access the particular methods. i.e.ClassA.queryArguments()
orClassB.queryVars()
If you want to create seperate object of
RequestReferrerHandler
class for ClassA and ClassB, you can extendRequestReferrerHandler
class to ClassA and ClassB without defining the constructor. So, when you create object of ClassA, it automatically inherit the constructor method ofRequestReferrerHandler
class and you can access the property and method byparent:
keyword. For example:You can do as same as for ClassB. Now you can create object for ClassA and ClassB assigning the constructor’s argument of their base class in Class C and use the return value of
ClassA.queryArguments()
orClassB.queryVars()
from their object.This problem can be simplified if you concentrated in the type of data instead of the meaning of the objects i were working in a api that is able to manipulate and get information from the Wordpress structure and i know that they already did a very nice job simplifying their data structure. so you only have to think in therms of post, post meta, and taxonomies (index) and not in meanings like (publication, tax, image, library, post, product).
Even in woo commerce you can see it that is very simple.
You would see that; a product is a type of post, that the data of the product is in post meta. a image is a type of post and the details of that image is in post_meta, that a news is a post and the details of this news s in post meta.
One structure different meaning. So for access everything or store data for everything you only need this elements. if you have access to this elements in a restful way the implementation is very easy to.
I hope this help you some how is just my point of view you can think otherwise. but i thought was important to share his with you.
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
andClassB
that both use common methods fromRequestReferrerHandler
. You already have the ground work laid out to do that with your interfaceRequestReferrerHandlerInterface
. So we'll say that you have an interface that looks like this:So long as this interface is implemented by the
RequestReferrerHandler
you can type hint the interface as the constructor requirements forClassA
andClassB
. 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 yourController
. In other words, do not inject$_GET
into your class. Make sure yourController
has all the information it needs to properly create theRequestReferrerHandler
Let's take a look at yourRequestReferrerHandler
class, flushed with all of the methods it will need.The second thing that sticks out is the
santize()
methods. Do you see how they are duplicated in bothClassA
andClassB
? ThesanitizeAuthor()
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.
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 thatsanitizeAuthor()
could be different amongClassA
andClassB
(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.Some things to take note of right off the bat. First, you'll notice there is the
setHandler()
method which accepts an instance of theRequestReferrerHandlerInterface
. 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 concreteRequestReferrerHandler
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 theRequestReferrerHandlerInterface
. 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 theRequestReferrerHandlerInterface
with methods that will guarantee their availability.Now, back to those sanitizers. We know that
ClassA
andClassB
will implement theirsanitizeAuthor()
methods differently. The abstract classAbstractSanitizer
was made the way it was because thesanitizeAuthor()
method from the theSanitizerInteface
isn't implemented inAbstractSanitizer
so we have to extend it to provide the functionality. We will need the following two classes to do this:These two concrete classes can be used with
ClassA
andClassB
to sanitize data within the concreteRequestReferrerHandler
methods that will be passed into them.So moving on, let's look at the spec for
ClassA
andClassB
. We know thatClassA
will need the methodqueryArguments()
,ClassB
will need the methodqueryVars()
and both classes will need to allow and instance of aRequestReferrerHandlerInterface
andSanitizerInterface
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 forClassA
andClassB
.Since we are now getting down to it, let's take a look at those classes that will use these.
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
andClassB
. We will type hint these by their respective interfaces as well.In your version of
queryArguments()
andqueryVars()
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.)Here's the output:
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
orQueryArgumentsInterface
. Consider these classesClassC
,ClassD
andClassE
. All of these classes will need a sanitizer class to go with them (that is ifSanitizerForClassA
orSanitizerForClassB
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 aGenericSanitizer
with a default implementation of thesanitizeAuthor()
method. Use can use this class withController::doStuff()
in any case where you don't need a specialized sanitizer class. You could just as easily implement different concrete classes ofQueryArgumentInterface
orQueryVarsInterface
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!