Skip to content

Instantly share code, notes, and snippets.

@brzuchal
Created July 3, 2023 11:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brzuchal/89e9481bbd34a6ce3d95a68eabff038b to your computer and use it in GitHub Desktop.
Save brzuchal/89e9481bbd34a6ce3d95a68eabff038b to your computer and use it in GitHub Desktop.
Interface Default Method Usecase
<?php
/**
* @template T
*/
class EntityResult {
public readonly $hasNextPage;
public function __construct(
public readonly array $entities,
public readonly int $page,
public readonly int $perPage,
public readonly int $total,
) {
$this->hasNextPage = $total > ($page * $perPage);
}
}
/**
* @template T
*/
interface EntityResource {
/**
* @return EntityResult<T>
*/
public function list(int $page = 1, int $perPage): EntityResult
/**
* @return iterable<T>
*/
public function stream(): iterable; // here the trait method can be implemented as default
}
trait IteratesEntityResult { // if interface has default method the trait is no longer needed
public function stream(): iterable {
$page = 1;
do {
$list = $this->list(page: $page++);
yield from $list->entities;
} while ($list->hasNextPage);
}
}
class GuzzleEntityResource implements EntityResource {
use IteratesEntityResult;
/**
* @return EntityResult<T>
*/
public function list(int $page = 1, int $perPage): EntityResult {
$entities = []; // fetch entities, and populate $page, $perPage and $total
return new EntityResult($entities, $page, $perPage, $total);
}
}
class DecoratedEntityResource implements EntityResource {
use IteratesEntityResult;
public function __construct(private EntityResource $decorated) {}
/**
* @return EntityResult<T>
*/
public function list(int $page = 1, int $perPage): EntityResult {
return $this->decorated->list($entities, $page, $perPage, $total);
}
}
@heiglandreas
Copy link

heiglandreas commented Jul 3, 2023

trait IteratesEntityResult { // if interface has default method the trait is no longer needed
    public function stream(): iterable {
        $page = 1;
        do {
            $list = $this->list(page: $page++);
            yield from $list->entities;
        } while ($list->hasNextPage);
    }

    abstract public function list(int $page = 1, int $perPage): EntityResult;
}

class GuzzleEntityResource {
    use IteratesEntityResult;
    /**
     * @return EntityResult<T>
     */
    public function list(int $page = 1, int $perPage): EntityResult {
        $entities = []; // fetch entities, and populate $page, $perPage and $total
        return new EntityResult($entities, $page, $perPage, $total);
    }
}
class DecoratedEntityResource {
    use IteratesEntityResult;
    public function __construct(private EntityResource $decorated) {}
    /**
     * @return EntityResult<T>
     */
    public function list(int $page = 1, int $perPage): EntityResult {
        return $this->decorated->list($entities, $page, $perPage, $total);
    }
}

@heiglandreas
Copy link

heiglandreas commented Jul 3, 2023

abstract class EntityResource { // if interface has default method the trait is no longer needed
    public function stream(): iterable {
        $page = 1;
        do {
            $list = $this->list(page: $page++);
            yield from $list->entities;
        } while ($list->hasNextPage);
    }

    abstract function list(int $page = 1, int $perPage): EntityResult;
}

class GuzzleEntityResource extends EntityResource {

    /**
     * @return EntityResult<T>
     */
    public function list(int $page = 1, int $perPage): EntityResult {
        $entities = []; // fetch entities, and populate $page, $perPage and $total
        return new EntityResult($entities, $page, $perPage, $total);
    }
}
class DecoratedEntityResource extends EntityResource {

    public function __construct(private EntityResource $decorated) {}
    /**
     * @return EntityResult<T>
     */
    public function list(int $page = 1, int $perPage): EntityResult {
        return $this->decorated->list($entities, $page, $perPage, $total);
    }
}

@brzuchal
Copy link
Author

brzuchal commented Jul 3, 2023

@heiglandreas you removed abstraction completely, the interface purpose was clear and can be mocked during the tests!
While with your suggestion this becomes very confusing to me. What should be used on the caller site if there is no interface?

In your argumentation from the ML you state that implementation details are something you consider bad and I agree this is what the interface was ensuring, the caller could restrict type to an interface and don't care about implementation details at all, when creating a mock only one method needs to be mocked as the second stream() would receive a default implementation from interface.

There is also a chance that two traits will carry the same stream() method which requires solving the conflict, else a fatal error is triggered, which is not true if you implement two interfaces that do have the same method stream() with default implementation - here first one is used.

@brzuchal
Copy link
Author

brzuchal commented Jul 3, 2023

@heiglandreas your second suggestion is valid but introduces inheritance.

@heiglandreas
Copy link

The second example uses an abstract class that can be used and mocked during tests.

When you have two stream methods in two different traits, then you have a problem anyhow as they are most certainly implementing different functionality. That would mean you have two interfaces declaring a stream method with two different signatures. And even f they use the same signature, are they really referencing the same implementation? Having a fatal error being triggered in that case makes absolute sense to me.

Otherwise you might have an interface that declares a stream method as default but the class you are adding that interface to already declares a stream method but with an implementation targeted at something completely different. Now the interfaces default would not be used but the classes implementation - which might do something completey unintended.

@brzuchal
Copy link
Author

brzuchal commented Jul 5, 2023

@heiglandreas Your proposal seems legit but there is one limitation caused by turning the interface into an abstract class, the inheritance is limited to only one parent.
Meaning that for all resource implementations using, for instance, Guzzle I prepared CommonGuzzleResource carrying helper methods designed for error checking, de-/serialization, etc. I cannot do that anymore since all descendants would have them because of inheritance.
Your solution is not free of defects, as it brings quite significant limitations in difference to interface default methods.

@heiglandreas
Copy link

heiglandreas commented Jul 5, 2023

I am well aware of that.

Adding implementation details to abstract contract definitions though is not the solution to that as it reduces interfaces to an implementation detail. That violates the separation of concerns and architectural principles. It also encourages sloppy design of interfaces to name a few drawbacks.

The current approach of using interfaces for the contract definition and backing these with traits for default implementations is the best option we have to separate the concerns of clean and abstract architectural design and business-specific implementation details.

An alternative to me would for example be a different way of handling traits in PHP. Like

trait EntityResourceImplementation implements EntityResource
{
    public function stream(): iterable {
        $page = 1;
        do {
            $list = $this->list(page: $page++);
            yield from $list->entities;
        } while ($list->hasNextPage);
    }
}

class GuzzleEntityResource use EntityResourceImplementation 
{
    /**
     * @return EntityResult<T>
     */
    public function list(int $page = 1, int $perPage): EntityResult {
        $entities = []; // fetch entities, and populate $page, $perPage and $total
        return new EntityResult($entities, $page, $perPage, $total);
    }
}
class DecoratedEntityResource use EntityResourceImplementation
{
    public function __construct(private EntityResource $decorated) {}
    /**
     * @return EntityResult<T>
     */
    public function list(int $page = 1, int $perPage): EntityResult {
        return $this->decorated->list($entities, $page, $perPage, $total);
    }
}

The difference here would be that the Interface would not be implemented by the class but by the Trait and thereby also implicitly by the class that uses that trait.

I'd rather favour explicitness and that way would still mean the interface needs to be declared somewhere but at least on the class level there is no need to think about two different entitities.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment