grifart/not-serializable

State explicitly that class has not been meant for serialization.

dev-master 2022-03-30 19:20 UTC

This package is auto-updated.

Last update: 2024-10-29 06:13:12 UTC


README

...and this package makes it easier for you.

A story

In 3/2022 this has happened.

class UserId { 
   private int $id;
   public function __construct(int $id) {$this->id = $id}
}

// later in an app
$userId = new UserId(42);
// ...
$_SESSION['user_id'] = $userId;

This will work. No warning, no errors, no problems. For now...

...

After a year you have decided to do internal change of UserId object – rename private property $id to $identifier.

class UserId { 
-   private int $id;
+   private int $identifier;
    public function __construct(int $id) {
-        $this->id = $id
+        $this->identifier = $id
    }
}

There is no public change of behaviour. There is no way outer observer should be able to find any changes in object behaviour. Static analysis passes, tests passes. All good! You deploy your application.

Boom! πŸ”₯ Application hard crashed for every user that has been logged-in on session deserialization. That is because implicit serialization made public interface from a private property.

Summary

Implicit serialization support is an explosive mine πŸ’₯, that you cannot easily spot from the surface or even in code-review. You have to inspect whole object life-cycle including code history. Almost impossible to spot.

This is very similar to disabling inheritance support from object which was not explicitly designed to be extended – final by default. You can find good reasoning at 1, 2.

TL;DR: You get much better object encapsulation, you know exactly how you object behaves.

The solution

πŸ‘‰ Replace implicit serialization with the explicit one.

1. Disabling implicit serialization

In PHP every object is serializable by default, until you say otherwise. So let's say otherwise.

This makes most sense for value objects and all its derivatives like entities. There is however no harm by disabling serialization support for all classes as you would never-ever want to serialize a service.

Open you PhpStorm settings and then Editor > File and Code Templates > Files > PHP Class and update the template.

final class ${NAME} {
	use \Grifart\NotSerializable\NoSerialization;
}

PhpStorm default template settings

Require this package using composer in your project

composer require grifart/not-serializable

It contains a simple trait, that disables serialization support for PHP >7.4. That is because serialization API has been changed php-watch.

2. Making the serialization explicit

When you need serialization support, implement serialization API explicitly.

Great thing is that when it is not possible to serialize by default, someone has to make a decision that we need it be serializable.

This allows us also choose right type of serialization:

a) Short term & simple

PHP serialization API: Allows you to quick start. Breaks when you start moving your classes around (heavily depends on our classes FQNs).

You can go this way by:

  1. removing the trait
  2. implementing __serialize() and __unserialize() methods.

b) Long term serialization

grifart/stateful: Provides strict versioning, field checking and FQN routing. Designed for:

  • serialization that can be deserialized after years (e.g. immutable event streams)
  • situation when codebase changes significantly
  • for maximal strictness, so it fails on you dev machine, not on production.

You can go this way by:

  1. removing the trait
  2. implementing Stateful interface & composer require grifart/stateful

Further development

  • PhpStan / Rector rule, that finds/fixes classes that does not have serialization disables and does NOT implement serialization explicitly (How to apply this only to value objects & derivatives?)