Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contracts example #75

Closed
wants to merge 15 commits into from
Closed

Contracts example #75

wants to merge 15 commits into from

Conversation

herbertroth
Copy link
Member

@herbertroth herbertroth commented Nov 27, 2024

This pull request includes changes to add an example for contracts.

@herbertroth herbertroth changed the title Update to latest version. Contracts example Nov 27, 2024
@herbertroth
Copy link
Member Author

@alexz707 @martineiber My idea how to implement this for separate public api and intern usage.

@@ -19,7 +19,8 @@
use Doctrine\DBAL\Connection;
use Pimcore\Db;

class DbResolver implements DbResolverInterface
// @internal
final class DbResolver implements DbResolverInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really get it. Should this also implement DbResolverContractInterface? Or what's the advantage of duplicating the interface?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why do we need the DbResolverContract decorator then?

Copy link
Member Author

@herbertroth herbertroth Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdreesen
Hi,
That’s a great question!

We recognize the problem and needs outlined in Issue #70 of this repository. Let me start by explaining the original intent behind the repository.

Our goal was to eliminate static calls, as they make it practically impossible to unit test classes that depend on them. To address this, we created a resolver bundle that uses a Resolver class with an interface. This approach allows us to create mocks or stubs from the interfaces.

These interfaces were never intended for use outside of this purpose, so we decided to add methods to the resolver and its interface only as needed. However, we didn’t anticipate that these interfaces could also be used in custom implementations for users’ own classes. This has introduced a challenge with semantic versioning whenever we add a new method to an interface, as described in Issue #70.

To solve this, we’ve discussed multiple approaches and have come up with a possible solution. The idea is to separate resolvers and interfaces into two categories: one for internal use and one stable version that adheres to semantic versioning.

Here’s how it would work:
Resolver and Interface

  • The Resolver class will be marked as final and designated for internal use only.
  • Similarly, the interface will also be marked as internal.
  • These internal components can evolve continuously, with new methods added as needed without adhering to semantic versioning.

Contract Resolver and Contract Interface

  • The “contract” resolver and interface will be part of the public API.
  • They will not be marked as final and will follow semantic versioning rules.
  • These components are designed for custom implementations by users.

For every major release, we will incorporate all additions made to the internal resolver and interface into the contract classes and interfaces, ensuring they remain up to date with new features while adhering to semantic versioning rules.

The difference between these two categories is subtle but impactful. Internal components provide flexibility for rapid iteration, while the public contract ensures stability for external use.

This is just an idea of how to handle this issue. We haven’t finalized a decision on how to proceed with this problem yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation. This is also what I thought this was supposed to be when I looked at it.
But the implementation doesn't make sense to me yet. What is the advantage if DbResolverContract is a decorator of the DbResolverInterface?
If you “only” want to add new methods, something like this should be enough, right?

interface DbResolverContractInterface
{
    public function getConnection(): Connection;

    public function reset(): Connection;

    public function get(): Connection;

    public function close(): void;
}

/**
 * @internal
 */
interface DbResolverInterface extends DbResolverContractInterface
{
    public function some();

    public function new();

    public function methods();
}

/**
 * @internal
 */
class DbResolver implements DbResolverInterface
{
    public function getConnection(): Connection
    {
        return Db::getConnection();
    }

    public function reset(): Connection
    {
        return Db::reset();
    }

    public function get(): Connection
    {
        return Db::get();
    }

    public function close(): void
    {
        Db::close();
    }

    public function some() 
    {
      // ...
    }

    public function new() 
    {
      // ...
    }

    public function methods() 
    {
      // ...
    }
}

You could then add new methods to the DbResolverInterface and implement them in DbResolver and in the next major move them from DbResolverInterface to DbResolverContractInterface. The DbResolverContract would not be needed.

But maybe I'm missing something 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdreesen this (your description) was also the first idea for me, when we discussed about it.

Copy link
Member Author

@herbertroth herbertroth Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdreesen @mattamon The problem is that the DbResolver is internal. This means that every time you want to use the contract interface, you either have to implement the class yourself or extend/use an internal class. One potential solution could be to make the DbResolver not internal. However, in doing so, the class would still implement an internal interface, which is also a bit awkward.

interface DataObjectResolverContractInterface
{
    public function getById(int|string $id, array $params = []): ?DataObject;

    public function getByPath(string $path, array $params = []): ?DataObject;

    public function getList(array $config = []): Listing;
}

/**
 * @internal
 */
interface DataObjectResolverInterface extends DataObjectResolverContractInterface
{
    public function getTypes(): array;

    public function setHideUnpublished(bool $hideUnpublished): void;
}

/**
 * @internal
 */
class DataObjectResolver implements DataObjectResolverInterface
{
    public function getById(int|string $id, array $params = []): ?DataObject
    {
        return DataObject::getById($id, $params);
    }

    public function getByPath(string $path, array $params = []): ?DataObject
    {
        return DataObject::getByPath($path, $params);
    }

    public function getList(array $config = []): Listing
    {
        return DataObject::getList($config);
    }

    public function getTypes(): array
    {
        return DataObject::getTypes();
    }

    public function setHideUnpublished(bool $hideUnpublished): void
    {
        DataObject::setHideUnpublished($hideUnpublished);
    }
}


interface CarResolverInterface extends DataObjectResolverContractInterface
{
    public function getByColor(string $color): Car;
}

// All fine till you need a class to extend.
class CarResolver extends DataObjectResolver implements CarResolverInterface
{
    public function getByColor(string $color): Car
    {
        return DataObject\Car::getByColor($color);
    }
}

// Or implement it by your own. But again a decorator. 
// It need to be done in every custom Resolver. 
// For major updates you have to modify it by your own risk.
class CarResolverWithDi implements CarResolverInterface
{

    public function __construct(private readonly CarResolverInterface $dataObjectResolver)
    {
    }

    public function getById(int|string $id, array $params = []): ?DataObject
    {
        return $this->dataObjectResolver->getById($id, $params);
    }

    public function getByPath(string $path, array $params = []): ?DataObject
    {
        return $this->dataObjectResolver->getByPath($path, $params);
    }

    public function getList(array $config = []): Listing
    {
        return $this->dataObjectResolver->getList($config);
    }
    
    public function getByColor(string $color): Car
    {
        DataObject\Car::getByColor($color);
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdreesen @mattamon I like the idea of splitting this up and do not use a decorator. Much simpler and cleaner. So still a Contract and Internal Class/Interface but like in you example, no decorator. Thanks for food for thought.

@herbertroth herbertroth linked an issue Nov 28, 2024 that may be closed by this pull request
martineiber and others added 11 commits November 28, 2024 12:14
* Add deprecations.

* Add final to deprecation.

* Add final to deprecation.

* Add final to deprecation.
# Conflicts:
#	src/Lib/Cache/RuntimeCacheResolver.php
#	src/Lib/CacheResolver.php
#	src/Lib/ToolResolver.php
#	src/Lib/ToolResolverInterface.php
#	src/Lib/Tools/Authentication/AuthenticationResolver.php
* Add final to all classes and mark all resolver as internal.

* Add Upgrade Notes
…s-Example

# Conflicts:
#	src/Db/DbResolver.php
#	src/Db/DbResolverInterface.php
Copy link

sonarcloud bot commented Nov 29, 2024

@github-actions github-actions bot locked and limited conversation to collaborators Nov 29, 2024
@herbertroth
Copy link
Member Author

Got some rebase problems on this. Will create a new one.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

BC break in #69
4 participants