Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

rhand's avatar
Level 6

testcase object expected, array given

Writing my first package, a Flyssytem Digital Ocean Adapter based on AWS S3 Adapter and Spatie's Flysytem for Dropbox.

Also beginning my first testCase. And based on the Spatie test case I am now adjusting things to work on mine. I have a it_can_write test that uses Argument. The third argument should be an object based on:

/**
     * Write a new file.
     *
     * @param string $path
     * @param string $contents
     * @param Config $config Config object
     *
     * @return false|array false on failure file meta data on success
     */
    public function write($path, $contents, Config $config)
    {
        return $this->upload($path, $contents, $config);
    }

But how does one check for that here:

public function it_can_write()
    {
        $this->client->upload(Argument::any(), Argument::any(), Argument::any())->willReturn([
            'server_modified' => '2015-05-12T15:50:38Z',
            'path_display' => '/prefix/something',
            '.tag' => 'file',
        ]);

        $result = $this->DigitalOceanAdapter->write('something', 'contents', new Config());

        $this->assertIsArrayType($result);
        $this->assertArrayHasKey('type', $result);
        $this->assertEquals('file', $result['type']);
    }

It only checks for any single value using Argument. And the final part is an object as the error also mentions:

Prophecy\Argument

Argument tokens shortcuts.

<?php
class Argument { }
@author Konstantin Kudryashov [email protected]

Expected type 'object'. Found 'array'.intelephense(1006)
View Problem (⌥F8)
No quick fixes available

How do you do that with phpspec?

0 likes
5 replies
rhand's avatar
Level 6

Updated start of test case and with

<?php

namespace Smart48\FlysystemDigitalOcean\Test;

use GuzzleHttp\Psr7\Response;
use League\Flysystem\Config;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Smart48\FlysystemDigitalOcean\DigitalOceanAdapter;

class DigitalOceanAdapterTest extends TestCase
{
    /** @var \Aws\S3\S3Client\Client|\Prophecy\Prophecy\ObjectProphecy */
    protected $client;

    /** @var \Smart48\FlysystemDigitalOcean\DigitalOceanAdapter */
    protected $DigitalOceanAdapter;
...

including comments with proper paths to S3 adapter and new adapter Visual Studio no longer complains. But composer test does nothing yet and functions like upload:

/** @test */
public function it_can_write()
{
    $this->client->upload(Argument::any(), Argument::any(), Argument::any())->willReturn([
        'server_modified' => '2015-05-12T15:50:38Z',
        'path_display' => '/prefix/something',
        '.tag' => 'file',
    ]);

    $result = $this->DigitalOceanAdapter->write('something', 'contents', new Config());

    $this->assertIsArrayType($result);
    $this->assertArrayHasKey('type', $result);
    $this->assertEquals('file', $result['type']);
}


cannot be directly located.

rhand's avatar
Level 6

Well, I can test, but had to specify file with proper path:

composer test tests/DigitalOceanAdapterTest.php  
> vendor/bin/phpunit 'tests/DigitalOceanAdapterTest.php'
PHPUnit 9.5.4 by Sebastian Bergmann and contributors.

EEEEEEEEEEEEEEEEEEEE                                              20 / 20 (100%)

Time: 00:00.047, Memory: 6.00 MB

There were 20 errors:

1) Smart48\FlysystemDigitalOcean\Test\DigitalOceanAdapterTest::it_can_write
TypeError: Argument 1 passed to Smart48\FlysystemDigitalOcean\DigitalOceanAdapter::__construct() must be an instance of Aws\S3\S3ClientInterface, instance of Double\stdClass\P1 given, called in /Users/jasperfrumau/code/flysystem-digital-ocean-spaces/tests/DigitalOceanAdapterTest.php on line 26

/Users/jasperfrumau/code/flysystem-digital-ocean-spaces/src/DigitalOceanAdapter.php:83
/Users/jasperfrumau/code/flysystem-digital-ocean-spaces/tests/DigitalOceanAdapterTest.php:26

2) Smart48\FlysystemDigitalOcean\Test\DigitalOceanAdapterTest::it_can_update
TypeError: Argument 1 passed to Smart48\FlysystemDigitalOcean\DigitalOceanAdapter::__construct() must be an instance of Aws\S3\S3ClientInterface, instance of Double\stdClass\P1 given, called in /Users/jasperfrumau/code/flysystem-digital-ocean-spaces/tests/DigitalOceanAdapterTest.php on line 26

/Users/jasperfrumau/code/flysystem-digital-ocean-spaces/src/DigitalOceanAdapter.php:83
/Users/jasperfrumau/code/flysystem-digital-ocean-spaces/tests/DigitalOceanAdapterTest.php:26

3) Smart48\FlysystemDigitalOcean\Test\DigitalOceanAdapterTest::it_can_write_a_stream
TypeError: Argument 1 passed to Smart48\FlysystemDigitalOcean\DigitalOceanAdapter::__construct() must be an instance of Aws\S3\S3ClientInterface, instance of Double\stdClass\P1 given, called in /Users/jasperfrumau/code/flysystem-digital-ocean-spaces/tests/DigitalOceanAdapterTest.php on line 26
...

Main issues now is

_construct() must be an instance of Aws\S3\S3ClientInterface, instance of Double\stdClass\P1 given
rhand's avatar
Level 6

Yes, I am aware of this and that is neat. We are, and it works with Spatie's Laravel Backup. But I keep on hitting 503 errors due to rate limiting using deleteObject, listObjects and headObject.

Suggestions Made

I was made to understand by Spatie whose Laravel Backup package I use it was best to create a custom adapter to deal with it or by adding some throttling (Github Issue comment). PHP League suggested adding a custom decorator, but I am not sure how I can add it to the existing adapter inside my Laravel package.

Custom PHP League Adapter

So... the package I was building is a custom adapter, but basically the S3 adapter with a decorator added. All rather new territory for me.. can't even get tests to run properly yet as that is also new terrain and perhaps overkill as well. But it is all I got so far.

Add Throttling / Backoff to Laravel Backup Package

Other option is perhaps adding throttle and or backoff Laravel methods to the backup jobs added by Laravel Backup, but then I wonder if the job will catch the error properly to trigger throttling / backoff so a job to clean oldest backup, list existing backups or meta is requeued when I hit an error 503 please slow down your request. Other issue is that I am not sure how I can extend a package with these customizations. I never extended a package before either.

rhand's avatar
Level 6

Well, thought about it some more and thought.. perhaps I should just add a custom Flysytem adapter using a service provider, wrap it to catch 503 and then extend the Laravel Backup Job to throttle and or backoff. So made app/Providers/SpacesServiceProvider.php:

<?php

namespace App\Providers;

use Storage;
use App\Flysystem\CheckFor503S3Client;
use Illuminate\Support\ServiceProvider;
use Aws\S3\S3Client as S3Client;
use League\Flysystem\AwsS3v3\AwsS3Adapter;
use League\Flysystem\Filesystem;

/**
 * Digital Ocean Spaces S3 Client extension
 * 
 * @link https://github.com/thephpleague/flysystem-aws-s3-v2/issues/18
 */
class DigitalOceanSpacesServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        // the laravel throtting and backoff functionality
        $this->app->bind('cart', 'App\Flysystem\FullThrottle');
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        Storage::extend('dospaces', function($app, $config) {
            $client = new S3Client([
                'visibility' => $config['private'],
                'key'    => $config['do_key'],
                'secret' => $config['do_secret'],
                'region' => $config['do_region'],
                'base_url' => $config['do_url'],
                'endpoint' => $config['do_endpoint'],
            ]);

            $wrappedClient = new CheckFor503S3Client($client);

            return new Filesystem(new AwsS3Adapter($wrappedClient, env('S3_BUCKET')));
        });
    }
}

and app/Console/Commands/BackupWithThrottleCommand.php:

<?php

namespace App\Console\Commands;

use Spatie\Backup\Commands\BackupCommand;
use Exception;
use Spatie\Backup\Events\BackupHasFailed;
use Spatie\Backup\Exceptions\InvalidCommand;
use Spatie\Backup\Tasks\Backup\BackupJobFactory;

class BackupWithThrottleCommand extends BackupCommand
{
    public function myMethod(){
        return 'myMethod';
    }
}

and used PHP League Flystem example to catch errors app/Flysystem/CheckFor503S3Client.php:

<?php

declare(strict_types=1);

namespace App\Flysystem;

use Aws\CommandInterface;
use Aws\S3\Exception\S3Exception;
use Aws\S3\S3ClientInterface;
use Exception;

class CheckFor503S3Client implements S3ClientInterface
{
    /**
     * @var S3ClientInterface
     */
    private $actualClient;

    /**
     * @var null|Exception
     */
    private $lastException = null;

    public function __construct(S3ClientInterface $actualClient)
    {
        $this->actualClient = $actualClient;
    }

    public function __call($name, array $arguments)
    {
        return $this->actualClient->__call($name, $arguments);
    }

    public function getCommand($name, array $args = [])
    {
        return $this->actualClient->getCommand($name, $args);
    }

    public function execute(CommandInterface $command)
    {
        try {
            $this->lastException = null;
            return $this->actualClient->execute($command);
        } catch (Exception $exception) {
            $this->lastException = $exception;
            throw $exception;
        }
    }

    public function was503()
    {
        if ( ! $this->lastException instanceof S3Exception) {
            return false;
        }
        
        return $this->lastException->getResponse()->getStatusCode() === 503;
    }

    public function executeAsync(CommandInterface $command)
    {
        return $this->actualClient->executeAsync($command);
    }

    public function getCredentials()
    {
        return $this->actualClient->getCredentials();
    }

    public function getRegion()
    {
        return $this->actualClient->getRegion();
    }

    public function getEndpoint()
    {
        return $this->actualClient->getEndpoint();
    }

    public function getApi()
    {
        return $this->actualClient->getApi();
    }

    public function getConfig($option = null)
    {
        return $this->actualClient->getConfig($option);
    }

    public function getHandlerList()
    {
        return $this->actualClient->getHandlerList();
    }

    public function getIterator($name, array $args = [])
    {
        return $this->actualClient->getIterator($name, $args);
    }

    public function getPaginator($name, array $args = [])
    {
        return $this->actualClient->getPaginator($name, $args);
    }

    public function waitUntil($name, array $args = [])
    {
        $this->actualClient->waitUntil($name, $args);
    }

    public function getWaiter($name, array $args = [])
    {
        return $this->actualClient->getWaiter($name, $args);
    }

    public function createPresignedRequest(CommandInterface $command, $expires, array $options = [])
    {
        return $this->actualClient->createPresignedRequest($command, $expires, $options);
    }

    public function getObjectUrl($bucket, $key)
    {
        return $this->actualClient->getObjectUrl($bucket, $key);
    }

    public function doesBucketExist($bucket)
    {
        return $this->actualClient->doesBucketExist($bucket);
    }

    public function doesObjectExist($bucket, $key, array $options = [])
    {
        return $this->actualClient->doesObjectExist($bucket, $key, $options);
    }

    public function registerStreamWrapper(): void
    {
        $this->actualClient->registerStreamWrapper();
    }

    public function deleteMatchingObjects($bucket, $prefix = '', $regex = '', array $options = [])
    {
        return $this->actualClient->deleteMatchingObjects($bucket, $prefix, $regex, $options);
    }

    public function deleteMatchingObjectsAsync($bucket, $prefix = '', $regex = '', array $options = [])
    {
        return $this->actualClient->deleteMatchingObjectsAsync($bucket, $prefix, $regex, $options);
    }

    public function upload($bucket, $key, $body, $acl = 'private', array $options = [])
    {
        return $this->actualClient->upload($bucket, $key, $body, $acl, $options);
    }

    public function uploadAsync($bucket, $key, $body, $acl = 'private', array $options = [])
    {
        return $this->actualClient->uploadAsync($bucket, $key, $body, $acl, $options);
    }

    public function copy($fromBucket, $fromKey, $destBucket, $destKey, $acl = 'private', array $options = [])
    {
        return $this->actualClient->copy($fromBucket, $fromKey, $destBucket, $destKey, $acl, $options);
    }

    public function copyAsync($fromBucket, $fromKey, $destBucket, $destKey, $acl = 'private', array $options = [])
    {
        return $this->actualClient->copyAsync($fromBucket, $fromKey, $destBucket, $destKey, $acl, $options);
    }

    public function uploadDirectory($directory, $bucket, $keyPrefix = null, array $options = [])
    {
        return $this->actualClient->uploadDirectory($directory, $bucket, $keyPrefix, $options);
    }

    public function uploadDirectoryAsync($directory, $bucket, $keyPrefix = null, array $options = [])
    {
        return $this->actualClient->uploadDirectoryAsync($directory, $bucket, $keyPrefix, $options);
    }

    public function downloadBucket($directory, $bucket, $keyPrefix = '', array $options = [])
    {
        return $this->actualClient->downloadBucket($directory, $bucket, $keyPrefix, $options);
    }

    public function downloadBucketAsync($directory, $bucket, $keyPrefix = '', array $options = [])
    {
        return $this->actualClient->downloadBucketAsync($directory, $bucket, $keyPrefix, $options);
    }

    public function determineBucketRegion($bucketName)
    {
        return $this->actualClient->determineBucketRegion($bucketName);
    }

    public function determineBucketRegionAsync($bucketName)
    {
        return $this->actualClient->determineBucketRegionAsync($bucketName);
    }
}

Not quite there but I think this could maybe work. What do you think @martinbean ?

Please or to participate in this conversation.