PHP is bad for Object-Oriented Programming OOP

PHP is bad for Object-Oriented Programming (aka OOP)

And I bet it also applicable for Python and JavaScript. In general, Javascript is anti-OOP.

While PHP allows to create classes, fields, methods, inheritance and even structs but the main problem is: PHP is a dynamic-type language (or weak type language).

For example this code works:

$var="hello";
$var=20;
echo $var;

The main problem with OOP is serialization. Let's say we have a customer system, where we list customers, and each customer has a category.

Customers

customerId name categoryId
1 John 1
2 Anna 1
3 Bob 2

Category

categoryId name
1 vip
2 normal
3 premium

It is our model(*) classes (or anemic object):

class Customer {
    var $customerId;
    var $name;
    var $category;  
}

class Category {
    var $categoryId;
    var $name;
}

Where the field category is of the class Category.

However, PHP doesn't know that.

What if we call the next code:

$obj=new Customer();
$obj->category->name="vip"; // Warning: Creating default object from empty value

It will crash. This also happens with other languages. In Java, it is called the billion dollar mistake.

So our class Customer could be written as follow.

class Customer {
    var $customerId;
    var $name;
    /** @var Category */
    var $category;

    public function __construct()
    {
        $this->category=new Category();
    }    
}

So we create automatically a new instance of Category each time we create an object or we could use the code as:

$obj=new Customer();
$obj->category=new Category();
$obj->category->name="vip";

(*) Laravel calls a MODEL class, a mix between a Model (anemic domain object) and a service (repository, Dao or DAL class) class. It is something unique. I don't know another language (or framework) that uses this definition of Model. For our exercise, Model = not the definition used by Laravel. It funny how those guys at Laravel breaks a definition while they try to impose their own rules (PSR).

Also, what if we use a field that does not exist?

$obj=new Customer();
$obj->wrongField="hi"; // this field does not exist and it doesn't crash.

PHP does know that the field doesn't exist, but it doesn't care.

PHP and JSON

Now, the main problem is JSON serialization. JSON is heavily used for PHP for share information (web service, cache, and so on).

Let's serialize and de-serialize our object (using JSON)

$obj=new Customer();
$jsonObj=json_encode($obj);
$stdObj=json_decode($jsonObj);

It is the result:

object(stdClass)#3 (3) {
  ["customerId"]=>
  int(1)
  ["name"]=>
  string(4) "John"
  ["category"]=>
  object(stdClass)#4 (2) {
    ["categoryId"]=>
    int(2)
    ["name"]=>
    string(3) "vip"
  }
}

json_decode returns a stdClass Object. It could be fine (because the data is in) but:

  • it skips the constructor, and this behavior is not obvious, i.e., prone of bugs that are hard to find.
  • We are unable to use any logic.
class Customer {
    /** @var DateTime */
    var $date;
    public function __construct()
    {
        $this->date=new DateTime();
    }    
}

$obj=new Customer();
$obj->date->setDate(2019,1,1);  // works
$jsonObj=json_encode($obj); // object (Customer)->json
$stdObj=json_decode($jsonObj); // json->object (stdClass)
$obj->date->setDate(2019,1,1); // Fatal error: Uncaught Error: Call to undefined method stdClass::setDate() in ...
  • it fails if we use type hinting, i.e. to call someFunction(Customer $customer) {...}
$obj=new Customer();
$jsonObj=json_encode($obj); // object (Customer)->json
$stdObj=json_decode($jsonObj); // json->object (stdClass)
someFunction($stdObj); // Fatal error: Uncaught TypeError: Argument 1 passed to someFunction() must be an instance of Customer, instance of stdClass given

function someFunction(Customer $customer) {
} 

Converting stdClass to named Class

However, it is possible to convert a stdClass object into a named class.

But it has a cost.

Let's say the next functions (they don't consider the field Category). I picked those because they are fast, other libraries are painfully slow.

function easyConvert($jsonTxt) {
   return str_replace(['O:8:"stdClass":3','O:8:"stdClass":2']
        ,['O:8:"Customer":3','O:8:"Category":2'],serialize(json_decode($jsonTxt)));    
}

function loopConvert($jsonTxt) {
    $data=json_decode($jsonTxt,true);
    $obj=new Customer();
    foreach ($data AS $key => $value) $obj->{$key} = $value;
    return $obj;
}

These are some functions (the first one is hacky) to converts a JSON string -> proper class.

benchmark time!

conversion time (sec)
arrays 0.0028018951416016
easyConvert 0.031358957290649
loopConvert 0.027074098587036

The first one calls the next method:

for($i=0;$i<10000;$i++) {
    $jsonArr=json_encode($objArr);
    $return=json_decode($jsonArr);
}

And the second and third uses the functions to returns a proper class.

In general, using classes vs. array is around x5 to x10 slower, and we are not considering that the field category is of the class Category, so the performance could be worst. It is not micro-optimization; it is a serious deal.

Some people claim: OPTIMIZE LATER, and it is valid for micro-optimizations but not for the model layer (that is the heart of the project).

What if our customer says "the system is fine, but it's slow, can you optimize it?". It could mean to rebuild a big part of our project. The model class: gone!. Every method that uses it: we need to rebuild it now with arrays. Every view layer that uses it: we need to rebuild it too. So we will end rebuilding the whole project.

Database and bad practice.

Let's say the next pseudo-code

$customers=getAllCustomers(); // it reads from the database the table customers
showTable($customers); // it shows a html table with the customers.

This code could work with little information, i.e. the information that we have on "dev ambiance".

But what if our system has 10 million customers.

  • The system will crash
  • The system will get a degraded performance.
  • The database will "collapse."
  • And other systems that use the database could also get a performance hit.

For our case, OOP could work fine with a small set of data, but it is not scalable. It's not rare to find a system that runs blazing fast, but it turns unusable at the end of the first year.

Now, let's say we use PDO to connects to the database.

function getAllCustomersArray() {
    $sth = $db->prepare("SELECT * FROM customers");
    $sth->execute();
    $result = $sth->fetchAll(PDO::FETCH_ASSOC);
}

function getAllCustomers() {
    $sth = $db->prepare("SELECT * FROM customers");
    $sth->execute();
    $result = $sth->fetchAll(PDO::FETCH_CLASS, "Customer");
}
function getCategory($id) {
    $sth = $db->prepare("SELECT * FROM category where idcategory=$id"); // use prepared statement!!
    $sth->execute();
    $result = $sth->fetchAll(PDO::FETCH_CLASS, "Category");
}
  • getAllCustomersArray() will return a flat array of declarative array.
  • getAllCustomers() will return a flat array of objects. However, it will not set correctly the field category then we should set it manually (for every row).

So, even when PDO is OOP, although the results are not suitable for every situation.

Some systems are simple bad designed. For example:

$customers=getAllCustomers(); // select * from customer
foreach($customers as $customer) {
    $customer->category=getCategory($customer->category->idCategory); // select * from category where idcategory=?
}
showTable($customers); 

In this example, we are doing a query to the table customer and "n" queries to the table "category". What if the table customer has 10000 rows? We will run 10001 queries!. And it is what some libraries do under the hood.

Now, it is a database-friendly function:

function getAllCustomersArray() {
    $sth = $db->prepare("SELECT customerId,customers.Name,categoryId,categories.Name NameCat FROM customers inner join categories");
    $sth->execute();
    $result = $sth->fetchAll(PDO::FETCH_ASSOC);
    foreach($result as &$row) {
        $row['category']=['categoryId'=>$row['categoryId'],'name'=>$row['NameCat']];
        unset($row['NameCat']);
        unset($row['categoryId']);
    }
}

We could do the same with classes too

function getAllCustomers() {
    $sth = $db->prepare("SELECT customerId,customers.Name,categoryId,categories.Name NameCat FROM customers inner join categories");
    $sth->execute();
    $result = $sth->fetchAll(PDO::FETCH_CLASS, "Customer");
    foreach($result as $row) {
        $row['category']=new Category($row->categoryId',$row['NameCat]);
        unset($row->NameCat);
        unset($row->categoryId);
    }
}

But it's a bit slower but it is not x5 or x10 slower, so it is acceptable.

Note: PDO::FETCH_CLASS uses the constructor (if any) after we set the values. It is not obvious. So we could/should use instead PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE. Working with models under PHP is tricky because we are forcing something that it's not, ahem, natural for the system.

But a synthetic benchmark doesn't reflex the reality.

Not really.

For example, what if we need to read a web service JSON Rest and it consists of 1000 rows. We need to de-serialize each row. And what if we also have 100 concurrent customers, so we to do the same 1000 x 100 times. With arrays, we could obtain the same performance 10 times faster (or to serve x10 more concurrent calls). And it is only if the class is simple. If the class is complex then the requirements could escalate. And we aren't used setter and getter. Setter and getter will overkill the performance.

Solution

If we want to use or consume JSON (especially if we need to read many JSON objects) then it's more suitable to use array.

Also, we could avoid generating dozen of Value Objects/DTO

We could even simulate constructors using functions.

function createCustomer($customerId,$name,$category) {
    return  ['customerId'=>$customerId,'name'=>$name,'category'=>$category];
}
function createCategory($categoryId,$name) {
    return  ['categoryId'=>$categoryId,'name'=>$name];
}

$newCustomer=createCustomer('1','John',createCategory('1','vip'));

Another solution

The second solution is to use the method serialize() unserialize() of PHP. They convert correctly an object. However, it is not suitable for a Web Service Rest

echo serialize($obj);

yields:

O:8:"Customer":3:{s:10:"customerId";i:1;s:4:"name";s:4:"John";s:8:"category";O:8:"Category":2:{s:10:"categoryId";i:2;s:4:"name";s:3:"vip";}}

However.

We couldn't work with setter and getter (unless we want to kill the performance).

Conclusion

It's easy to debug and develop the code using classes, especially if the IDE has some type-hinting feature such as PHPStorm:

But it is not for every situation.

For example, OOP works with service classes, controller class, and any class that doesn't involve serialization or a long set of objects.

Also, if we use PHPStorm, we could simulate type-hinting using https://plugins.jetbrains.com/plugin/9927-deep-assoc-completion (it's a free plugin). It's not the same than to use objects; nevertheless, it's close.

Note: The image of the title is from http://www.phpthewrongway.com/

Conclusion 2

But then, is it PHP a bad language?.

In fact no. It's hard to develop but it's more flexible than Java or C# (strong-typed languages), for instance, we don't need DTO/Value Object and we don't need to create a new class if we need to pass a complex argument of a function.

About Jorge Castro

Currently: Entrepreneur and Private Consultant
Civil Engineer in Informatics - USACH Chile.
Master in Business Administration (MBA) CEPADE Spain
Microsoft Certified Professional
Oracle Certified Associate
ScrumMaster Certified
Former developer
Former Project Manager

Related Posts