Completed
Push — master ( 932cd1...29fc25 )
by Christopher
01:11
created

BaseService::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 55 and the first side effect is on line 52.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
namespace POData;
4
5
use POData\Common\MimeTypes;
6
use POData\Common\Version;
7
use POData\ObjectModel\IObjectSerialiser;
8
use POData\OperationContext\HTTPRequestMethod;
9
use POData\Common\ErrorHandler;
10
use POData\Common\Messages;
11
use POData\Common\ODataException;
12
use POData\Common\ODataConstants;
13
use POData\Common\NotImplementedException;
14
use POData\Common\InvalidOperationException;
15
use POData\Common\HttpStatus;
16
use POData\Providers\Metadata\Type\IType;
17
use POData\Providers\ProvidersWrapper;
18
use POData\Providers\Stream\StreamProviderWrapper;
19
use POData\Configuration\ServiceConfiguration;
20
use POData\UriProcessor\UriProcessor;
21
use POData\UriProcessor\RequestDescription;
22
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetKind;
23
use POData\OperationContext\ServiceHost;
24
use POData\Providers\Metadata\ResourceType;
25
use POData\Providers\Metadata\Type\Binary;
26
use POData\ObjectModel\ObjectModelSerializer;
27
use POData\Writers\Atom\AtomODataWriter;
28
use POData\Writers\Json\JsonLightMetadataLevel;
29
use POData\Writers\Json\JsonLightODataWriter;
30
use POData\Writers\Json\JsonODataV1Writer;
31
use POData\Writers\Json\JsonODataV2Writer;
32
use POData\Writers\ResponseWriter;
33
use POData\Providers\Query\IQueryProvider;
34
use POData\Providers\Metadata\IMetadataProvider;
35
use POData\OperationContext\IOperationContext;
36
use POData\Writers\ODataWriterRegistry;
37
38
/**
39
 * Class BaseService.
40
 *
41
 * The base class for all BaseService specific classes. This class implements
42
 * the following interfaces:
43
 *  (1) IRequestHandler
44
 *      Implementing this interface requires defining the function
45
 *      'handleRequest' that will be invoked by dispatcher
46
 *  (2) IService
47
 *      Force BaseService class to implement functions for custom
48
 *      data service providers
49
 */
50
// TODO: Look at ripping this out after phockito gets binned
51
if (class_exists('POData\BaseService')) {
52
    return;
53
}
54
55
abstract class BaseService implements IRequestHandler, IService
56
{
57
    /**
58
     * The wrapper over IQueryProvider and IMetadataProvider implementations.
59
     *
60
     * @var ProvidersWrapper
61
     */
62
    private $providersWrapper;
63
64
    /**
65
     * The wrapper over IStreamProvider implementation.
66
     *
67
     * @var StreamProviderWrapper
68
     */
69
    private $_streamProvider;
70
71
    /**
72
     * Hold reference to the ServiceHost instance created by dispatcher,
73
     * using this library can access headers and body of Http Request
74
     * dispatcher received and the Http Response Dispatcher is going to send.
75
     *
76
     * @var ServiceHost
77
     */
78
    private $_serviceHost;
79
80
    /**
81
     * To hold reference to ServiceConfiguration instance where the
82
     * service specific rules (page limit, resource set access rights
83
     * etc...) are defined.
84
     *
85
     * @var ServiceConfiguration
86
     */
87
    private $config;
88
89
    /**
90
     * Hold reference to object serialiser - bit wot turns PHP objects
91
     * into message traffic on wire
92
     *
93
     * @var IObjectSerialiser
94
     */
95
    protected $objectSerialiser;
96
97
    protected function __construct(IObjectSerialiser $serialiser = null)
98
    {
99
        if (null == $serialiser) {
100
            $this->objectSerialiser = new ObjectModelSerializer($this, null);
101
        } else {
102
            $this->objectSerialiser = $serialiser;
103
        }
104
    }
105
106
    /**
107
     * Gets reference to ServiceConfiguration instance so that
108
     * service specific rules defined by the developer can be
109
     * accessed.
110
     *
111
     * @return ServiceConfiguration
112
     */
113
    public function getConfiguration()
114
    {
115
        return $this->config;
116
    }
117
118
    //TODO: shouldn't we hide this from the interface..if we need it at all.
119
    /**
120
     * Get the wrapper over developer's IQueryProvider and IMetadataProvider implementation.
121
     *
122
     * @return ProvidersWrapper
123
     */
124
    public function getProvidersWrapper()
125
    {
126
        return $this->providersWrapper;
127
    }
128
129
    /**
130
     * Gets reference to wrapper class instance over IDSSP implementation.
131
     *
132
     * @return StreamProviderWrapper
133
     */
134
    public function getStreamProviderWrapper()
135
    {
136
        return $this->_streamProvider;
137
    }
138
139
    /**
140
     * Get reference to the data service host instance.
141
     *
142
     * @return ServiceHost
143
     */
144
    public function getHost()
145
    {
146
        return $this->_serviceHost;
147
    }
148
149
    /**
150
     * Sets the data service host instance.
151
     *
152
     * @param ServiceHost $serviceHost The data service host instance
153
     */
154
    public function setHost(ServiceHost $serviceHost)
155
    {
156
        $this->_serviceHost = $serviceHost;
157
    }
158
159
    /**
160
     * To get reference to operation context where we have direct access to
161
     * headers and body of Http Request we have received and the Http Response
162
     * We are going to send.
163
     *
164
     * @return IOperationContext
165
     */
166
    public function getOperationContext()
167
    {
168
        return $this->_serviceHost->getOperationContext();
169
    }
170
171
    /**
172
     * Get reference to the wrapper over IStreamProvider or
173
     * IStreamProvider2 implementations.
174
     *
175
     * @return StreamProviderWrapper
176
     */
177
    public function getStreamProvider()
178
    {
179
        if (is_null($this->_streamProvider)) {
180
            $this->_streamProvider = new StreamProviderWrapper();
181
            $this->_streamProvider->setService($this);
182
        }
183
184
        return $this->_streamProvider;
185
    }
186
187
    /**
188
     * Top-level handler invoked by Dispatcher against any request to this
189
     * service. This method will hand over request processing task to other
190
     * functions which process the request, set required headers and Response
191
     * stream (if any in Atom/Json format) in
192
     * WebOperationContext::Current()::OutgoingWebResponseContext.
193
     * Once this function returns, dispatcher uses global WebOperationContext
194
     * to write out the request response to client.
195
     * This function will perform the following operations:
196
     * (1) Check whether the top level service class implements
197
     *     IServiceProvider which means the service is a custom service, in
198
     *     this case make sure the top level service class implements
199
     *     IMetaDataProvider and IQueryProvider.
200
     *     These are the minimal interfaces that a custom service to be
201
     *     implemented in order to expose its data as OData. Save reference to
202
     *     These interface implementations.
203
     *     NOTE: Here we will ensure only providers for IDSQP and IDSMP. The
204
     *     IDSSP will be ensured only when there is an GET request on MLE/Named
205
     *     stream.
206
     *
207
     * (2). Invoke 'Initialize' method of top level service for
208
     *      collecting the configuration rules set by the developer for this
209
     *      service.
210
     *
211
     * (3). Invoke the Uri processor to process the request URI. The uri
212
     *      processor will do the following:
213
     *      (a). Validate the request uri syntax using OData uri rules
214
     *      (b). Validate the request using metadata of this service
215
     *      (c). Parse the request uri and using, IQueryProvider
216
     *           implementation, fetches the resources pointed by the uri
217
     *           if required
218
     *      (d). Build a RequestDescription which encapsulate everything
219
     *           related to request uri (e.g. type of resource, result
220
     *           etc...)
221
     * (3). Invoke handleRequest2 for further processing
222
     */
223
    public function handleRequest()
224
    {
225
        try {
226
            $this->createProviders();
227
            $this->_serviceHost->validateQueryParameters();
228
            //$requestMethod = $this->getOperationContext()->incomingRequest()->getMethod();
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
229
            //if ($requestMethod != HTTPRequestMethod::GET()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
57% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
230
            // Now supporting GET and trying to support PUT
231
            //throw ODataException::createNotImplementedError(Messages::onlyReadSupport($requestMethod));
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
232
            //}
233
234
            $uriProcessor = UriProcessor::process($this);
235
            $request = $uriProcessor->getRequest();
236
            $this->serializeResult($request, $uriProcessor);
237
        } catch (\Exception $exception) {
238
            ErrorHandler::handleException($exception, $this);
239
            // Return to dispatcher for writing serialized exception
240
            return;
241
        }
242
    }
243
244
    /**
245
     * @return IQueryProvider
246
     */
247
    abstract public function getQueryProvider();
248
249
    /**
250
     * @return IMetadataProvider
251
     */
252
    abstract public function getMetadataProvider();
253
254
    /**
255
     * @return \POData\Providers\Stream\IStreamProvider
256
     */
257
    abstract public function getStreamProviderX();
258
259
    /** @var ODataWriterRegistry */
260
    private $writerRegistry;
261
262
    /**
263
     * Returns the ODataWriterRegistry to use when writing the response to a service document or resource request.
264
     *
265
     * @return ODataWriterRegistry
266
     */
267
    public function getODataWriterRegistry()
268
    {
269
        return $this->writerRegistry;
270
    }
271
272
    /**
273
     * This method will query and validates for IMetadataProvider and IQueryProvider implementations, invokes
274
     * BaseService::Initialize to initialize service specific policies.
275
     *
276
     *
277
     * @throws ODataException
278
     */
279
    protected function createProviders()
280
    {
281
        $metadataProvider = $this->getMetadataProvider();
282
        if (is_null($metadataProvider)) {
283
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
284
        }
285
286
        if (!is_object($metadataProvider) || !$metadataProvider instanceof IMetadataProvider) {
287
            throw ODataException::createInternalServerError(Messages::invalidMetadataInstance());
288
        }
289
290
        $queryProvider = $this->getQueryProvider();
291
292
        if (is_null($queryProvider)) {
293
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
294
        }
295
296
        if (!is_object($queryProvider)) {
297
            throw ODataException::createInternalServerError(Messages::invalidQueryInstance());
298
        }
299
300
        if (!$queryProvider instanceof IQueryProvider) {
301
            throw ODataException::createInternalServerError(Messages::invalidQueryInstance());
302
        }
303
304
        $this->config = new ServiceConfiguration($metadataProvider);
305
        $this->providersWrapper = new ProvidersWrapper(
306
            $metadataProvider,
307
            $queryProvider,
308
            $this->config
309
        );
310
311
        $this->initialize($this->config);
312
313
        //TODO: this seems like a bad spot to do this
314
        $this->writerRegistry = new ODataWriterRegistry();
315
        $this->registerWriters();
316
    }
317
318
    //TODO: i don't want this to be public..but it's the only way to test it right now...
319
    public function registerWriters()
320
    {
321
        $registry = $this->getODataWriterRegistry();
322
        $serviceVersion = $this->getConfiguration()->getMaxDataServiceVersion();
323
        $serviceURI = $this->getHost()->getAbsoluteServiceUri()->getUrlAsString();
324
325
        //We always register the v1 stuff
326
        $registry->register(new JsonODataV1Writer());
327
        $registry->register(new AtomODataWriter($serviceURI));
328
329
        if ($serviceVersion->compare(Version::v2()) > -1) {
330
            $registry->register(new JsonODataV2Writer());
331
        }
332
333
        if ($serviceVersion->compare(Version::v3()) > -1) {
334
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::NONE(), $serviceURI));
335
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::MINIMAL(), $serviceURI));
336
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::FULL(), $serviceURI));
337
        }
338
    }
339
340
    /**
341
     * Serialize the requested resource.
342
     *
343
     * @param RequestDescription $request      The description of the request  submitted by the client
344
     * @param UriProcessor       $uriProcessor Reference to the uri processor
345
     */
346
    protected function serializeResult(RequestDescription $request, UriProcessor $uriProcessor)
347
    {
348
        $isETagHeaderAllowed = $request->isETagHeaderAllowed();
349
        if ($this->config->getValidateETagHeader() && !$isETagHeaderAllowed) {
350
            if (!is_null($this->_serviceHost->getRequestIfMatch())
351
                || !is_null($this->_serviceHost->getRequestIfNoneMatch())
352
            ) {
353
                throw ODataException::createBadRequestError(
354
                    Messages::eTagCannotBeSpecified($this->getHost()->getAbsoluteRequestUri()->getUrlAsString())
355
                );
356
            }
357
        }
358
359
        $responseContentType = self::getResponseContentType($request, $uriProcessor, $this);
360
361
        if (is_null($responseContentType) && $request->getTargetKind() != TargetKind::MEDIA_RESOURCE()) {
362
            //the responseContentType can ONLY be null if it's a stream (media resource) and that stream is storing null as the content type
363
            throw new ODataException(Messages::unsupportedMediaType(), 415);
364
        }
365
366
        $odataModelInstance = null;
367
        $hasResponseBody = true;
368
        // Execution required at this point if request target to any resource other than
369
370
        // (1) media resource - For Media resource 'getResponseContentType' already performed execution as it needs to know the mime type of the stream
371
        // (2) metadata - internal resource
372
        // (3) service directory - internal resource
373
        if ($request->needExecution()) {
374
            $method = $this->_serviceHost->getOperationContext()->incomingRequest()->getMethod();
375
            $uriProcessor->execute();
376
            if ($method == HTTPRequestMethod::DELETE()) {
377
                $this->_serviceHost->setResponseStatusCode(HttpStatus::CODE_OK);
378
                return;
379
            }
380
381
            $objectModelSerializer = new ObjectModelSerializer($this, $request);
382
            $method = ($method != HTTPRequestMethod::POST());
383
            if (!$request->isSingleResult() && $method) {
384
                // Code path for collection (feed or links)
385
                $entryObjects = $request->getTargetResult();
386
                assert(is_array($entryObjects), '!is_array($entryObjects)');
387
                // If related resource set is empty for an entry then we should
388
                // not throw error instead response must be empty feed or empty links
389
                if ($request->isLinkUri()) {
390
                    $odataModelInstance = $objectModelSerializer->writeUrlElements($entryObjects);
391
                    assert(
392
                        $odataModelInstance instanceof \POData\ObjectModel\ODataURLCollection,
393
                        '!$odataModelInstance instanceof ODataURLCollection'
394
                    );
395
                } else {
396
                    $odataModelInstance = $objectModelSerializer->writeTopLevelElements($entryObjects);
397
                    assert(
398
                        $odataModelInstance instanceof \POData\ObjectModel\ODataFeed,
399
                        '!$odataModelInstance instanceof ODataFeed'
400
                    );
401
                }
402
            } else {
403
                // Code path for entry, complex, bag, resource reference link,
404
                // primitive type or primitive value
405
                $result = $request->getTargetResult();
406
                $requestTargetKind = $request->getTargetKind();
407
                if ($request->isLinkUri()) {
408
                    // In the query 'Orders(1245)/$links/Customer', the targeted
409
                    // Customer might be null
410
                    if (is_null($result)) {
411
                        throw ODataException::createResourceNotFoundError(
412
                            $request->getIdentifier()
413
                        );
414
                    }
415
416
                    $odataModelInstance = $objectModelSerializer->writeUrlElement($result);
417
                } elseif ($requestTargetKind == TargetKind::RESOURCE()) {
418
                    if (!is_null($this->_serviceHost->getRequestIfMatch())
419
                        && !is_null($this->_serviceHost->getRequestIfNoneMatch())
420
                    ) {
421
                        throw ODataException::createBadRequestError(
422
                            Messages::bothIfMatchAndIfNoneMatchHeaderSpecified()
423
                        );
424
                    }
425
                    // handle entry resource
426
                    $needToSerializeResponse = true;
427
                    $targetResourceType = $request->getTargetResourceType();
428
                    $eTag = $this->compareETag(
429
                        $result,
430
                        $targetResourceType,
0 ignored issues
show
Bug introduced by
It seems like $targetResourceType defined by $request->getTargetResourceType() on line 427 can be null; however, POData\BaseService::compareETag() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
431
                        $needToSerializeResponse
432
                    );
433
434
                    if ($needToSerializeResponse) {
435
                        if (is_null($result)) {
436
                            // In the query 'Orders(1245)/Customer', the targeted
437
                            // Customer might be null
438
                            // set status code to 204 => 'No Content'
439
                            $this->_serviceHost->setResponseStatusCode(HttpStatus::CODE_NOCONTENT);
440
                            $hasResponseBody = false;
441
                        } else {
442
                            $odataModelInstance = $objectModelSerializer->writeTopLevelElement($result);
443
                        }
444
                    } else {
445
                        // Resource is not modified so set status code
446
                        // to 304 => 'Not Modified'
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
447
                        $this->_serviceHost->setResponseStatusCode(HttpStatus::CODE_NOT_MODIFIED);
448
                        $hasResponseBody = false;
449
                    }
450
451
                    // if resource has eTagProperty then eTag header needs to written
452
                    if (!is_null($eTag)) {
453
                        $this->_serviceHost->setResponseETag($eTag);
454
                    }
455
                } elseif ($requestTargetKind == TargetKind::COMPLEX_OBJECT()) {
456
                    $odataModelInstance = $objectModelSerializer->writeTopLevelComplexObject(
457
                        $result,
458
                        $request->getProjectedProperty()->getName(),
459
                        $request->getTargetResourceType()
0 ignored issues
show
Bug introduced by
$request->getTargetResourceType() cannot be passed to writetoplevelcomplexobject() as the parameter $resourceType expects a reference.
Loading history...
Bug introduced by
It seems like $request->getTargetResourceType() can be null; however, writeTopLevelComplexObject() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
460
                    );
461
                } elseif ($requestTargetKind == TargetKind::BAG()) {
462
                    $odataModelInstance = $objectModelSerializer->writeTopLevelBagObject(
463
                        $result,
464
                        $request->getProjectedProperty()->getName(),
465
                        $request->getTargetResourceType(),
0 ignored issues
show
Bug introduced by
$request->getTargetResourceType() cannot be passed to writetoplevelbagobject() as the parameter $resourceType expects a reference.
Loading history...
Bug introduced by
It seems like $request->getTargetResourceType() can be null; however, writeTopLevelBagObject() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
466
                        $odataModelInstance
0 ignored issues
show
Unused Code introduced by
The call to ObjectModelSerializer::writeTopLevelBagObject() has too many arguments starting with $odataModelInstance.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
467
                    );
468
                } elseif ($requestTargetKind == TargetKind::PRIMITIVE()) {
469
                    $odataModelInstance = $objectModelSerializer->writeTopLevelPrimitive(
470
                        $result,
471
                        $request->getProjectedProperty(),
0 ignored issues
show
Bug introduced by
$request->getProjectedProperty() cannot be passed to writetoplevelprimitive() as the parameter $resourceProperty expects a reference.
Loading history...
Bug introduced by
It seems like $request->getProjectedProperty() can be null; however, writeTopLevelPrimitive() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
472
                        $odataModelInstance
0 ignored issues
show
Unused Code introduced by
The call to ObjectModelSerializer::writeTopLevelPrimitive() has too many arguments starting with $odataModelInstance.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
473
                    );
474
                } elseif ($requestTargetKind == TargetKind::PRIMITIVE_VALUE()) {
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
475
                    // Code path for primitive value (Since its primitve no need for
476
                    // object model serialization)
477
                    // Customers('ANU')/CompanyName/$value => string
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
478
                    // Employees(1)/Photo/$value => binary stream
479
                    // Customers/$count => string
480
                } else {
481
                    assert(false, 'Unexpected resource target kind');
482
                }
483
            }
484
        }
485
486
        //Note: Response content type can be null for named stream
487
        if ($hasResponseBody && !is_null($responseContentType)) {
488
            if ($request->getTargetKind() != TargetKind::MEDIA_RESOURCE() && $responseContentType != MimeTypes::MIME_APPLICATION_OCTETSTREAM) {
489
                //append charset for everything except:
490
                //stream resources as they have their own content type
491
                //binary properties (they content type will be App Octet for those...is this a good way? we could also decide based upon the projected property
492
493
                $responseContentType .= ';charset=utf-8';
494
            }
495
        }
496
497
        if ($hasResponseBody) {
498
            ResponseWriter::write(
499
                $this,
500
                $request,
501
                $odataModelInstance,
502
                $responseContentType
503
            );
504
        }
505
    }
506
507
    /**
508
     * Gets the response format for the requested resource.
509
     *
510
     * @param RequestDescription $request      The request submitted by client and it's execution result
511
     * @param UriProcessor       $uriProcessor The reference to the UriProcessor
512
     * @param IService           $service      Reference to the service implementation instance
513
     *
514
     * @return string the response content-type, a null value means the requested resource
515
     *                is named stream and IDSSP2::getStreamContentType returned null
516
     *
517
     * @throws ODataException, HttpHeaderFailure
518
     */
519
    public static function getResponseContentType(
520
        RequestDescription $request,
521
        UriProcessor $uriProcessor,
522
        IService $service
523
    ) {
524
525
        // The Accept request-header field specifies media types which are acceptable for the response
526
527
        $host = $service->getHost();
528
        $requestAcceptText = $host->getRequestAccept();
529
        $requestVersion = $request->getResponseVersion();
530
531
        //if the $format header is present it overrides the accepts header
532
        $format = $host->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_FORMAT);
533
        if (!is_null($format)) {
534
            //There's a strange edge case..if application/json is supplied and it's V3
535
            if ($format == MimeTypes::MIME_APPLICATION_JSON && $requestVersion == Version::v3()) {
536
                //then it's actual minimalmetadata
537
                //TODO: should this be done with the header text too?
538
                $format = MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META;
539
            }
540
541
            $requestAcceptText = ServiceHost::translateFormatToMime($requestVersion, $format);
542
        }
543
544
        //The response format can be dictated by the target resource kind. IE a $value will be different then expected
545
        //getTargetKind doesn't deal with link resources directly and this can change things
546
        $targetKind = $request->isLinkUri() ? TargetKind::LINK() : $request->getTargetKind();
547
548
        switch ($targetKind) {
549
            case TargetKind::METADATA():
550
                return HttpProcessUtility::selectMimeType(
551
                    $requestAcceptText,
552
                    array(MimeTypes::MIME_APPLICATION_XML)
553
                );
554
555 View Code Duplication
            case TargetKind::SERVICE_DIRECTORY():
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...
556
                return HttpProcessUtility::selectMimeType(
557
                    $requestAcceptText,
558
                    array(
559
                        MimeTypes::MIME_APPLICATION_ATOMSERVICE,
560
                        MimeTypes::MIME_APPLICATION_XML,
561
                        MimeTypes::MIME_APPLICATION_JSON,
562
                        MimeTypes::MIME_APPLICATION_JSON_FULL_META,
563
                        MimeTypes::MIME_APPLICATION_JSON_NO_META,
564
                        MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META,
565
                        MimeTypes::MIME_APPLICATION_JSON_VERBOSE,
566
567
                    )
568
                );
569
570
            case TargetKind::PRIMITIVE_VALUE():
571
                $supportedResponseMimeTypes = array(MimeTypes::MIME_TEXTPLAIN);
572
573
                if ($request->getIdentifier() != '$count') {
574
                    $projectedProperty = $request->getProjectedProperty();
575
                    assert(!is_null($projectedProperty), 'is_null($projectedProperty)');
576
                    $type = $projectedProperty->getInstanceType();
577
                    assert($type instanceof IType, '!$type instanceof IType');
578
                    if ($type instanceof Binary) {
579
                        $supportedResponseMimeTypes = array(MimeTypes::MIME_APPLICATION_OCTETSTREAM);
580
                    }
581
                }
582
583
                return HttpProcessUtility::selectMimeType(
584
                    $requestAcceptText,
585
                    $supportedResponseMimeTypes
586
                );
587
588
            case TargetKind::PRIMITIVE():
589
            case TargetKind::COMPLEX_OBJECT():
590
            case TargetKind::BAG():
591 View Code Duplication
            case TargetKind::LINK():
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...
592
                return HttpProcessUtility::selectMimeType(
593
                    $requestAcceptText,
594
                    array(
595
                        MimeTypes::MIME_APPLICATION_XML,
596
                        MimeTypes::MIME_TEXTXML,
597
                        MimeTypes::MIME_APPLICATION_JSON,
598
                        MimeTypes::MIME_APPLICATION_JSON_FULL_META,
599
                        MimeTypes::MIME_APPLICATION_JSON_NO_META,
600
                        MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META,
601
                        MimeTypes::MIME_APPLICATION_JSON_VERBOSE,
602
                    )
603
                );
604
605 View Code Duplication
            case TargetKind::RESOURCE():
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...
606
                return HttpProcessUtility::selectMimeType(
607
                    $requestAcceptText,
608
                    array(
609
                        MimeTypes::MIME_APPLICATION_ATOM,
610
                        MimeTypes::MIME_APPLICATION_JSON,
611
                        MimeTypes::MIME_APPLICATION_JSON_FULL_META,
612
                        MimeTypes::MIME_APPLICATION_JSON_NO_META,
613
                        MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META,
614
                        MimeTypes::MIME_APPLICATION_JSON_VERBOSE,
615
                    )
616
                );
617
618
            case TargetKind::MEDIA_RESOURCE():
619
                if (!$request->isNamedStream() && !$request->getTargetResourceType()->isMediaLinkEntry()) {
620
                    throw ODataException::createBadRequestError(
621
                        Messages::badRequestInvalidUriForMediaResource(
622
                            $host->getAbsoluteRequestUri()->getUrlAsString()
623
                        )
624
                    );
625
                }
626
627
                $uriProcessor->execute();
628
                $request->setExecuted();
629
                // DSSW::getStreamContentType can throw error in 2 cases
630
                // 1. If the required stream implementation not found
631
                // 2. If IDSSP::getStreamContentType returns NULL for MLE
632
                $responseContentType = $service->getStreamProviderWrapper()
633
                    ->getStreamContentType(
634
                        $request->getTargetResult(),
635
                        $request->getResourceStreamInfo()
0 ignored issues
show
Bug introduced by
It seems like $request->getResourceStreamInfo() can be null; however, getStreamContentType() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
636
                    );
637
638
                // Note StreamWrapper::getStreamContentType can return NULL if the requested named stream has not
639
                // yet been uploaded. But for an MLE if IDSSP::getStreamContentType returns NULL then StreamWrapper will throw error
640
                if (!is_null($responseContentType)) {
641
                    $responseContentType = HttpProcessUtility::selectMimeType(
642
                        $requestAcceptText,
643
                        array($responseContentType)
644
                    );
645
                }
646
647
                return $responseContentType;
648
        }
649
650
        //If we got here, we just don't know what it is...
651
        throw new ODataException(Messages::unsupportedMediaType(), 415);
652
    }
653
654
    /**
655
     * For the given entry object compare it's eTag (if it has eTag properties)
656
     * with current eTag request headers (if it present).
657
     *
658
     * @param mixed        &$entryObject             entity resource for which etag
659
     *                                               needs to be checked
660
     * @param ResourceType &$resourceType            Resource type of the entry
661
     *                                               object
662
     * @param bool         &$needToSerializeResponse On return, this will contain
663
     *                                               True if response needs to be
664
     *                                               serialized, False otherwise
665
     * @param bool         $needToSerializeResponse
666
     *
667
     * @return string|null The ETag for the entry object if it has eTag properties
668
     *                     NULL otherwise
669
     */
670
    protected function compareETag(
671
        &$entryObject,
672
        ResourceType & $resourceType,
673
        &$needToSerializeResponse
674
    ) {
675
        $needToSerializeResponse = true;
676
        $eTag = null;
677
        $ifMatch = $this->_serviceHost->getRequestIfMatch();
678
        $ifNoneMatch = $this->_serviceHost->getRequestIfNoneMatch();
679
        if (is_null($entryObject)) {
680
            if (!is_null($ifMatch)) {
681
                throw ODataException::createPreConditionFailedError(
682
                    Messages::eTagNotAllowedForNonExistingResource()
683
                );
684
            }
685
686
            return null;
687
        }
688
689
        if ($this->config->getValidateETagHeader() && !$resourceType->hasETagProperties()) {
690
            if (!is_null($ifMatch) || !is_null($ifNoneMatch)) {
691
                // No eTag properties but request has eTag headers, bad request
692
                throw ODataException::createBadRequestError(
693
                    Messages::noETagPropertiesForType()
694
                );
695
            }
696
697
            // We need write the response but no eTag header
698
            return null;
699
        }
700
701
        if (!$this->config->getValidateETagHeader()) {
702
            // Configuration says do not validate ETag so we will not write ETag header in the
703
            // response even though the requested resource support it
704
            return null;
705
        }
706
707
        if (is_null($ifMatch) && is_null($ifNoneMatch)) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
708
            // No request eTag header, we need to write the response
709
            // and eTag header
710
        } elseif (strcmp($ifMatch, '*') == 0) {
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
711
            // If-Match:* => we need to write the response and eTag header
712
        } elseif (strcmp($ifNoneMatch, '*') == 0) {
713
            // if-None-Match:* => Do not write the response (304 not modified),
714
            // but write eTag header
715
            $needToSerializeResponse = false;
716
        } else {
717
            $eTag = $this->getETagForEntry($entryObject, $resourceType);
718
            // Note: The following code for attaching the prefix W\"
719
            // and the suffix " can be done in getETagForEntry function
720
            // but that is causing an issue in Linux env where the
721
            // firefix browser is unable to parse the ETag in this case.
722
            // Need to follow up PHP core devs for this.
723
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
724
            if (!is_null($ifMatch)) {
725
                if (strcmp($eTag, $ifMatch) != 0) {
726
                    // Requested If-Match value does not match with current
727
                    // eTag Value then pre-condition error
728
                    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
729
                    throw ODataException::createPreConditionFailedError(
730
                        Messages::eTagValueDoesNotMatch()
731
                    );
732
                }
733
            } elseif (strcmp($eTag, $ifNoneMatch) == 0) {
734
                //304 not modified, but in write eTag header
735
                $needToSerializeResponse = false;
736
            }
737
        }
738
739
        if (is_null($eTag)) {
740
            $eTag = $this->getETagForEntry($entryObject, $resourceType);
741
            // Note: The following code for attaching the prefix W\"
742
            // and the suffix " can be done in getETagForEntry function
743
            // but that is causing an issue in Linux env where the
744
            // firefix browser is unable to parse the ETag in this case.
745
            // Need to follow up PHP core devs for this.
746
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
747
        }
748
749
        return $eTag;
750
    }
751
752
    /**
753
     * Returns the etag for the given resource.
754
     * Note: This function will not add W\" prefix and " suffix, its callers
755
     * repsonsability.
756
     *
757
     * @param mixed        &$entryObject  Resource for which etag value needs to
758
     *                                    be returned
759
     * @param ResourceType &$resourceType Resource type of the $entryObject
760
     *
761
     * @return string|null ETag value for the given resource (with values encoded
762
     *                     for use in a URI) there are etag properties, NULL if
763
     *                     there is no etag property
764
     */
765
    protected function getETagForEntry(&$entryObject, ResourceType & $resourceType)
766
    {
767
        $eTag = null;
768
        $comma = null;
769
        foreach ($resourceType->getETagProperties() as $eTagProperty) {
770
            $type = $eTagProperty->getInstanceType();
771
            assert($type instanceof IType, '!$type instanceof IType');
772
773
            $value = null;
774
            try {
775
                //TODO #88...also this seems like dupe work
776
                $reflectionProperty = new \ReflectionProperty($entryObject, $eTagProperty->getName());
777
                $value = $reflectionProperty->getValue($entryObject);
778
            } catch (\ReflectionException $reflectionException) {
779
                throw ODataException::createInternalServerError(
780
                    Messages::failedToAccessProperty($eTagProperty->getName(), $resourceType->getName())
781
                );
782
            }
783
784 View Code Duplication
            if (is_null($value)) {
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...
785
                $eTag = $eTag . $comma . 'null';
786
            } else {
787
                $eTag = $eTag . $comma . $type->convertToOData($value);
788
            }
789
790
            $comma = ',';
791
        }
792
793 View Code Duplication
        if (!is_null($eTag)) {
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...
794
            // If eTag is made up of datetime or string properties then the above
795
            // IType::convertToOData will perform utf8 and url encode. But we don't
796
            // want this for eTag value.
797
            $eTag = urldecode(utf8_decode($eTag));
798
799
            return rtrim($eTag, ',');
800
        }
801
802
        return null;
803
    }
804
805
    /**
806
     * This function will perform the following operations:
807
     * (1) Invoke delegateRequestProcessing method to process the request based
808
     *     on request method (GET, PUT/MERGE, POST, DELETE)
809
     * (3) If the result of processing of request needs to be serialized as HTTP
810
     *     response body (e.g. GET request result in single resource or resource
811
     *     collection, successful POST operation for an entity need inserted
812
     *     entity to be serialized back etc..), Serialize the result by using
813
     *     'serializeReultForResponseBody' method
814
     *     Set the serialized result to
815
     *     WebOperationContext::Current()::OutgoingWebResponseContext::Stream.
816
     */
817
    protected function handleRequest2()
818
    {
819
    }
820
821
    /**
822
     * This method will perform the following operations:
823
     * (1) If request method is GET, then result is already there in the
824
     *     RequestDescription so simply return the RequestDescription
825
     * (2). If request method is for CDU
826
     *      (Create/Delete/Update - POST/DELETE/PUT-MERGE) hand
827
     *      over the responsibility to respective handlers. The handler
828
     *      methods are:
829
     *      (a) handlePOSTOperation() => POST
830
     *      (b) handlePUTOperation() => PUT/MERGE
831
     *      (c) handleDELETEOperation() => DELETE
832
     * (3). Check whether its required to write any result to the response
833
     *      body
834
     *      (a). Request method is GET
835
     *      (b). Request is a POST for adding NEW Entry
836
     *      (c). Request is a POST for adding Media Resource Stream
837
     *      (d). Request is a POST for adding a link
838
     *      (e). Request is a DELETE for deleting entry or relationship
839
     *      (f). Request is a PUT/MERGE for updating an entry
840
     *      (g). Request is a PUT for updating a link
841
     *     In case a, b and c we need to write the result to response body,
842
     *     for d, e, f and g no body content.
843
     *
844
     * @return RequestDescription|null Instance of RequestDescription with
845
     *                                 result to be write back Null if no result to write
846
     */
847
    protected function delegateRequestProcessing()
848
    {
849
    }
850
851
    /**
852
     * Serialize the result in the current request description using
853
     * appropriate odata writer (AtomODataWriter/JSONODataWriter).
854
     */
855
    protected function serializeReultForResponseBody()
856
    {
857
    }
858
859
    /**
860
     * Handle POST request.
861
     *
862
     *
863
     * @throws NotImplementedException
864
     */
865
    protected function handlePOSTOperation()
866
    {
867
    }
868
869
    /**
870
     * Handle PUT/MERGE request.
871
     *
872
     *
873
     * @throws NotImplementedException
874
     */
875
    protected function handlePUTOperation()
876
    {
877
    }
878
879
    /**
880
     * Handle DELETE request.
881
     *
882
     *
883
     * @throws NotImplementedException
884
     */
885
    protected function handleDELETEOperation()
886
    {
887
    }
888
}
889