Completed
Push — master ( 1b2d0a...89ea1a )
by Oscar
03:00
created

MethodOverride::parameter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 10
rs 9.4285
cc 2
eloc 5
nc 2
nop 2
1
<?php
2
3
namespace Psr7Middlewares\Middleware;
4
5
use Psr7Middlewares\Middleware;
6
use Psr\Http\Message\RequestInterface;
7
use Psr\Http\Message\ResponseInterface;
8
9
/**
10
 * Middleware to override the request method using http headers.
11
 */
12
class MethodOverride
13
{
14
    const HEADER = 'X-Http-Method-Override';
15
16
    /**
17
     * @var array Allowed methods overrided in GET
18
     */
19
    private $get = ['HEAD', 'CONNECT', 'TRACE', 'OPTIONS'];
20
21
    /**
22
     * @var array Allowed methods overrided in POST
23
     */
24
    private $post = ['PATCH', 'PUT', 'DELETE', 'COPY', 'LOCK', 'UNLOCK'];
25
26
    /**
27
     * @var null|string The POST parameter name
28
     */
29
    private $postParam;
30
31
    /**
32
     * @var null|string The GET parameter name
33
     */
34
    private $getParam;
35
36
37
    /**
38
     * Set allowed method for GET.
39
     *
40
     * @return self
41
     */
42
    public function get(array $methods)
43
    {
44
        $this->get = $methods;
45
46
        return $this;
47
    }
48
49
    /**
50
     * Set allowed method for POST.
51
     *
52
     * @return self
53
     */
54
    public function post(array $methods)
55
    {
56
        $this->post = $methods;
57
58
        return $this;
59
    }
60
61
    /**
62
     * Configure the parameters.
63
     * 
64
     * @param string $name
65
     * @param bool   $get
66
     *
67
     * @return self
68
     */
69
    public function parameter($name, $get = true)
70
    {
71
        $this->postParam = $name;
72
73
        if ($get) {
74
            $this->getParam = $name;
75
        }
76
77
        return $this;
78
    }
79
80
    /**
81
     * Execute the middleware.
82
     *
83
     * @param RequestInterface  $request
84
     * @param ResponseInterface $response
85
     * @param callable          $next
86
     *
87
     * @return ResponseInterface
88
     */
89
    public function __invoke(RequestInterface $request, ResponseInterface $response, callable $next)
90
    {
91
        $method = $this->getOverrideMethod($request);
92
93
        if (!empty($method)) {
94
            $allowed = $this->getAllowedOverrideMethods($request);
95
96
            if (!empty($allowed)) {
97
                if (in_array($method, $allowed)) {
98
                    $request = $request->withMethod($method);
99
                } else {
100
                    return $response->withStatus(405);
101
                }
102
            }
103
        }
104
105
        return $next($request, $response);
106
    }
107
108
    /**
109
     * Returns the override method.
110
     * 
111
     * @param RequestInterface $request
112
     * 
113
     * @return string|null
114
     */
115
    private function getOverrideMethod(RequestInterface $request)
116
    {
117 View Code Duplication
        if ($request->getMethod() === 'POST' && $this->postParam !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
118
            $params = $request->getParsedBody();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Http\Message\RequestInterface as the method getParsedBody() does only exist in the following implementations of said interface: Zend\Diactoros\ServerRequest.

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...
119
120
            if (isset($params[$this->postParam])) {
121
                return strtoupper($params[$this->postParam]);
122
            }
123
        }
124
125 View Code Duplication
        if ($request->getMethod() === 'GET' && $this->getParam !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
126
            $params = $request->getQueryParams();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Http\Message\RequestInterface as the method getQueryParams() does only exist in the following implementations of said interface: Zend\Diactoros\ServerRequest.

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...
127
128
            if (isset($params[$this->getParam])) {
129
                return strtoupper($params[$this->getParam]);
130
            }
131
        }
132
133
        $method = $request->getHeaderLine(self::HEADER);
134
135
        if (!empty($method) && ($method !== $request->getMethod())) {
136
            return strtoupper($method);
137
        }
138
    }
139
140
    /**
141
     * Returns the allowed override methods.
142
     * 
143
     * @param RequestInterface $request
144
     * 
145
     * @return array
146
     */
147
    private function getAllowedOverrideMethods(RequestInterface $request)
148
    {
149
        switch ($request->getMethod()) {
150
            case 'GET':
151
                return $this->get;
152
153
            case 'POST':
154
                return $this->post;
155
156
            default:
157
                return [];
158
        }
159
    }
160
}
161