can I bind interface to my service and call method statically?

Viewed 33

There is already interface that I don`t want to change it or edit it.

<?php

namespace App\Contracts;

interface MyInterface
{
    public function set(array $data): bool;
}

And I created a service.

<?php

namespace App\Services;

use App\Contracts\MyInterface;

class MyService implements MyInterface
{
    protected $data;

    public function set(array $data): bool
    {
        try {
            $this->data = $data;
            return true;

        } catch (\Throwable $th) {
            return false;
        }
    }
}

and the question is, is there any way that I can call set method statically without editing MyInterface? (for example in binding)

$service = app(MyInterface::class);
$service::set(["some-key"=> "some-value"]);
1 Answers

Your best bet is to use two different design patterns, singleton and static proxy

First we'll set up our existing interface and service (I added a __toString for demo purposes)

interface MyInterface
{
    public function set(array $data): bool;
}

class MyService implements MyInterface
{
    protected $data;
    
    public function set(array $data): bool
    {
        try
        {
            $this->data = $data;
            return true;
            
        }
        catch (\Throwable $th)
        {
            return false;
        }
    }
    
    public function __toString()
    {
        return json_encode($this->data, JSON_PRETTY_PRINT);
    }
}

The first piece of the puzzle, a static singleton factory. All this really does is hold a single instance of our service class. When you call getInstance, the instance is returned. We create the instance the first time the method is called.

/**
 * Manage a singleton instance of MyService
 */
class MyServiceSingletonFactory
{
    /**
     * @var MyService|null Singleton instance
     */
    private static ?MyService $instance;
    
    /**
     * @var bool initialization status
     */
    private static bool $initialized = false;
    
    /**
     * Return the singleton instance, create if it has not been initialized
     * @return MyService
     */
    public static function getInstance(): MyService
    {
        /*
         * We can't access a typed static property before it's initialized,
         * so we need to track init with a boolean instead of relying on instanceof
         */
        if (!self::$initialized)
        {
            self::$instance    = new MyService();
            self::$initialized = true;
        }
        
        return self::$instance;
    }
}

This is all we really need in order to get static access to a service instance, but for maximum convenience, we can add a static proxy layer.

This pattern has become known as a facade because Laravel calls what are essentially static proxies facades, but traditionally the facade pattern has been used to provide a simplified interface to a complex set of classes, and this isn't that.

We'll implement an interface to enforce parity with MyInterface, although it isn't strictly necessary.

/**
 * This isn't really necessary because we don't pass instances of these,
 * but it does enforce the implementation of a uniform interface
 */
interface MyServiceProxyInterface
{
    public static function set(array $data): bool;
    
    public static function print(): string;
}

/**
 * Provides a static interface to MyService
 */
class MyServiceProxy implements MyServiceProxyInterface
{
    /**
     * Pass through to the set method on the singleton instance of MyService
     * @param array $data
     * @return bool
     */
    public static function set(array $data): bool
    {
        return MyServiceSingletonFactory::getInstance()->set($data);
    }
    
    public static function print(): string
    {
        return MyServiceSingletonFactory::getInstance()->__toString();
    }
}

Now we have everything we need, let's run this code through its paces.

$data = [
    'foo'     => 'bar',
    'wombats' => true
];

MyServiceSingletonFactory::getInstance()->set($data);

// Verify that our data is set
echo MyServiceSingletonFactory::getInstance() . PHP_EOL;

// Uses the same instance, will print the data we set previously
echo MyServiceProxy::print() . PHP_EOL;

$data2 = [
    'foo'     => 'baz',
    'wombats' => false
];

// Update via the static proxy
MyServiceProxy::set($data2);

// Inspect updated data
echo MyServiceProxy::print() . PHP_EOL;

/**
 * Requires an instance of MyService
 * @param MyService $myService
 * @return void
 */
function useMyService(MyService $myService): void
{
    $myService->set([
                        'foo'     => 'tree',
                        'wombats' => null
                    ]);
}

// We can send the instance to something that requires a MyService instance...
useMyService(MyServiceSingletonFactory::getInstance());

// ...and our static singleton instance will reflect any changes made
echo MyServiceProxy::print() . PHP_EOL;
Related