bmitch / Codor

Custom PHPCS sniffs to find Code Smells

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Sniff warning when method returns a primitive

bmitch opened this issue · comments

Just a warning to suggest to the developer to see if an object could be returned instead.

Primitives idea is good but it can be improved.

In my opinion when sending/returning an array or an object that can be modified is the most important thing that we should prevent when communicating between objects.

Arrays are like a black box, we never know what are really inside them until we open them, therefore checks must be performed before do it. Also they are mutable, therefore open the doors for inconsistencies in our code.

Objects are not immutable and they are also an open door for bugs and inconsistency across the code, once they state can be inadvertently changed once they are passed around by reference, therefore they MUST NOT be used to pass data between objects.

The solution is to enforce the use of Immutable Value Objects to sent/return data when it needs to be passed between objects.

So the check should ensure that the returned object is an Immutable Value Object, but here is where things get really hard once we don't have such thing natively in PHP.

An Immutable Value Object Must:

  • be a final class so that further manipulation or extending is denied.
  • have all properties declared with visibility as private.
  • have properties that can only be accessed by getters.
  • not contain setters methods.
  • not use magic methods to access is properties.
  • not use methods to rebuild itself with other values.
  • be constructed by passing the values in the constructor.
  • not contain logic to manipulate/calculate values.
  • should implement a trait to enforce immutability as much as possible.

Trait Example

This trait is extracted from production code.

<?php declare(strict_types = 1);

namespace Nn4m\ImmutableValuesObject\Traits;

use LogicException;

/**
 * @author  Exadra37     <exadra37ingmailpointcom>
 * @since   12 April 2017
 */

/**
 * Let's try to enforce as much as possible that the Immutable Values Object we create using this Trait can't be mutated.
 *
 * Ensures we can't:
 *     - get any porperty that does not have the Accessor public method.
 *     - set any property.
 *     - unset a property.
 *     - perform validations like isset() or empty() on properties.
 *     - clone the object.
 *
 * Remember also to declare the Class as final so it can't be extended.
 */
trait EnforceImmutableValuesObjectTrait
{
    /**
     * Don't allow to call a property directly.
     *
     * This will enforce the use of Accessors methods to get their values.
     *
     * @throws LogicException When we try to call directly a property in the class.
     *
     * @param  string $propertyName The property name.
     *
     * @return void
     */
    public function __get(string $propertyName)
    {
        throw new LogicException("Can't get property {$propertyName} directly on a Immutable Object at {$this->buildCaller(debug_backtrace()[1])}");
    }

    /**
     * Don't allow to set a value in any property of the Class.
     *
     * Also prevents the creation of properties in the class.
     *
     * @throws LogicException When we try to set or create a property in the class.
     *
     * @param  string $propertyName The property name.
     * @param  mix    $value        The proerty value to be setted.
     *
     * @return  void
     */
    public function __set(string $propertyName, $value)
    {
        throw new LogicException("Can't set property {$propertyName} on a Immutable Object at {$this->buildCaller(debug_backtrace()[1])}");
    }

    /**
     * Don't allow to unset a property in the class.
     *
     * @throws LogicException When we try to unset a property in the class.
     *
     * @param  string $propertyName The property name.
     *
     * @return void
     */
    public function __unset(string $propertyName)
    {
        throw new LogicException("Can't unset property {$propertyName} on a Immutable Object at {$this->buildCaller(debug_backtrace()[1])}");
    }

    /**
     * Don't allow to perform isset() and empty() checks against a property in the class.
     *
     * @throws LogicException When we try to run isset() or empty() on a class property.
     *
     * @param  string $propertyName The property name.
     *
     * @return void
     */
    public function __isset(string $propertyName)
    {
        throw new LogicException("Can't perform check in property '{$propertyName}' on a Immutable Object at {$this->buildCaller(debug_backtrace()[1])}");
    }

    /**
     * Don't allow the class to be cloned.
     *
     * @throws LogicException When we try to clone the class.
     *
     * @return void
     */
    public function __clone()
    {
        throw new LogicException("Does not make sense to clone a Immutable Object at {$this->buildCaller(debug_backtrace()[1])}");
    }

    /**
     * Build the caller that triggered the exception.
     *
     * We handle it differently depending if the caller is a class or just a plain php file.
     *
     * @link(Issue #3, http://gitlab.nn4m.net/nn4m/immutable-values-object/issues/3)
     *
     * @param  array $trace The debug back trace from php.
     *
     * @return string with the name of the file/class, plus function/method name and line.
     */
    private function buildCaller(array $trace): string
    {
        if (!empty($trace['class'])) {

            return "{$trace['class']}{$trace['type']}{$trace['function']}():{$trace['line']}";
        }

        return "{$trace['file']} at function {$trace['function']}() on line {$trace['line']}";
    }
}

So if accepted my idea, the challenge here is how to check for an Immutable Value Object that matches the definition I propose above.

Thanks @Exadra37

I understand your point but I'm not sure if it is acceptable to ensure that every object returned from a method is a Immutable Value Object.

There are many times where the value returned from a method will be an object but not an immutable one.

My main goal with this sniff was to produce warnings when a method returns a primitive so that it gives the developer an opportunity to evaluate if it is possible to return an object instead.