Completed
Pull Request — master (#1)
by Spencer
02:07
created

ScaleEngineRequestVisitor::_getSignature()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
namespace FloSports\ScaleEngine\Visitor;
3
4
use Guzzle\Http\Message\RequestInterface;
5
use Guzzle\Service\Command\CommandInterface;
6
use Guzzle\Service\Command\LocationVisitor\Request\AbstractRequestVisitor;
7
use Guzzle\Service\Description\Parameter;
8
use SplObjectStorage;
9
10
/**
11
 * Visitor used to apply a parameter to an array that will be signed and
12
 * serialized as a POST field named json in the response in accordance with
13
 * ScaleEngine's API format.
14
 */
15
class ScaleEngineRequestVisitor extends AbstractRequestVisitor
16
{
17
    /** @type \SplObjectStorage Data object for persisting JSON data. */
18
    private $_storage;
19
20
    /** @type string The API secret used to sign requests. */
21
    private $_apiSecret;
22
23
    /**
24
     * Create the visitor, initializing the SPL Object Storage.
25
     *
26
     * @param string $apiSecret The API secret used to sign requests.
27
     */
28
    public function __construct($apiSecret)
29
    {
30
        $this->_storage = new SplObjectStorage();
31
        $this->_apiSecret = $apiSecret;
32
    }
33
34
    /**
35
     * Handle a single parameter for a command.
36
     *
37
     * Each command has it's own storage inside the SPL Object Storage.  This
38
     * adds the parameter/value to the storage for that command, after
39
     * processing any filters, etc. for the parameter.
40
     *
41
     * @param CommandInterface $command The command the parameter is for.
42
     * @param RequestInterface $request The HTTP request being prepared.
43
     *     Unused.
44
     * @param Parameter $param The parameter definition being set.
45
     * @param mixed $value The value the parameter is being set to.
46
     * @return void
47
     */
48
    public function visit(CommandInterface $command, RequestInterface $request, Parameter $param, $value)
49
    {
50
        $data = isset($this->_storage[$command]) ? $this->_storage[$command] : [];
51
        $data[$param->getWireName()] = $this->prepareValue($value, $param);
52
        $this->_storage[$command] = $data;
53
    }
54
55
    /**
56
     * Finalizes the data for a request and sets it in the response.
57
     *
58
     * ScaleEngine requires a signature of the request to be added to the
59
     * request.  The data is then JSON-encoded and shoved into a POST field
60
     * named `json`.
61
     *
62
     * @param CommandInterface $command The command being sent.
63
     * @param RequestInterface $request The HTTP request being prepared.  Will
64
     *     be modified with the serialized data added as a POST field.
65
     * @return void
66
     */
67
    public function after(CommandInterface $command, RequestInterface $request)
68
    {
69
        if (!isset($this->_storage[$command])) {
70
            return;
71
        }
72
73
        $data = $this->_storage[$command];
74
        unset($this->_storage[$command]);
75
76
        $data['timestamp'] = isset($data['timestamp']) ? $data['timestamp'] : time();
77
        $data['signature'] = $this->_getSignature($data);
78
79
        $request->setPostField('json', json_encode($data));
1 ignored issue
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Guzzle\Http\Message\RequestInterface as the method setPostField() does only exist in the following implementations of said interface: Guzzle\Http\Message\EntityEnclosingRequest.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
80
    }
81
82
    /**
83
     * Gets the signature of the data using ScaleEngine's algorithm.
84
     *
85
     * This is a potentially fragile method as it could break if `json_encode`
86
     * changes.
87
     *
88
     * @param array $data The data from the request to sign.
89
     * @return string The base64-encoded signature of the data.
90
     */
91
    private function _getSignature(array $data)
92
    {
93
        $json = json_encode($data);
94
95
        return base64_encode(hash_hmac('sha256', $json, $this->_apiSecret, true));
96
    }
97
}
98