Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
project
subrequests
Commits
09ef18a4
Unverified
Commit
09ef18a4
authored
Mar 19, 2017
by
Mateu Aguiló Bosch
Browse files
Initial commit
parent
7d90f293
Changes
11
Hide whitespace changes
Inline
Side-by-side
src/Blueprint/Parser.php
0 → 100644
View file @
09ef18a4
<?php
namespace
Drupal\subrequests\Blueprint
;
use
Symfony\Component\HttpFoundation\Request
;
use
Symfony\Component\HttpFoundation\Response
;
use
Symfony\Component\Serializer\SerializerInterface
;
/**
* TODO: Change this comment. We'll use the serializer instead.
* Base class for blueprint parsers. There may be slightly different blueprint
* formats depending on the encoding. For instance, JSON encoded blueprints will
* reference other properties in the responses using JSON pointers, while XML
* encoded blueprints will use XPath.
*/
class
Parser
{
/**
* @var \Symfony\Component\Serializer\SerializerInterface
*/
protected
$serializer
;
/**
* The Mime-Type of the incoming requests.
*
* @var string
*/
protected
$type
;
/**
* Parser constructor.
*/
public
function
__construct
(
SerializerInterface
$serializer
)
{
$this
->
serializer
=
$serializer
;
}
/**
* @param \Symfony\Component\HttpFoundation\Request $request
* The master request to parse. We need from it:
* - Request body content.
* - Request mime-type.
*/
public
function
parseRequest
(
Request
$request
)
{
$tree
=
$this
->
serializer
->
deserialize
(
$request
->
getContent
(),
RequestTree
::
class
,
$request
->
getRequestFormat
()
);
$request
->
attributes
->
add
(
RequestTree
::
SUBREQUEST_TREE
,
$tree
);
// It assumed that all subrequests use the same Mime-Type.
$this
->
type
=
$request
->
getMimeType
(
$request
->
getRequestFormat
());
}
/**
* @param \Symfony\Component\HttpFoundation\Response[] $responses
* The responses to combine.
*
* @return \Symfony\Component\HttpFoundation\Response
* The combined response with a 207.
*/
public
function
combineResponses
(
array
$responses
)
{
$delimiter
=
md5
(
microtime
());
// Prepare the root content type header.
$content_type
=
sprintf
(
'multipart/related; boundary="%s", type=%s'
,
$delimiter
,
$this
->
type
);
$headers
=
[
'Content-Type'
=>
$content_type
];
$context
=
[
'delimiter'
=>
$delimiter
];
$content
=
$this
->
serializer
->
serialize
(
$responses
,
'multipart-related'
,
$context
);
return
Response
::
create
(
$content
,
207
,
$headers
);
}
/**
* Validates if a request can be constituted from this payload.
*
* @param array $data
* The user data representing a sub-request.
*
* @return bool
* TRUE if the data is valid. FALSE otherwise.
*/
public
static
function
isValidSubrequest
(
array
$data
)
{
// TODO: Implement this!
return
(
bool
)
$data
;
}
}
src/Blueprint/RequestTree.php
0 → 100644
View file @
09ef18a4
<?php
namespace
Drupal\subrequests\Blueprint
;
use
Symfony\Component\HttpFoundation\Request
;
/**
* Contains the hierarchical information of the requests.
*/
class
RequestTree
{
const
SUBREQUEST_TREE
=
'_subrequests_tree_object'
;
const
SUBREQUEST_ID
=
'_subrequests_content_id'
;
const
SUBREQUEST_DONE
=
'_subrequests_is_done'
;
/**
* @var \Symfony\Component\HttpFoundation\Request[]
*/
protected
$requests
;
/**
* If this tree sprouts from another requests, save the request id here.
* @var string
*/
protected
$parentId
;
/**
* RequestTree constructor.
*
* @param \Symfony\Component\HttpFoundation\Request[] $requests
* @param string $parent_id
*/
public
function
__construct
(
array
$requests
,
$parent_id
=
NULL
)
{
$this
->
requests
=
$requests
;
$this
->
parentId
=
$parent_id
;
}
/**
* Gets a flat list of the initialized requests for the current level.
*
* All requests returned by this method can run in parallel. If a request has
* children requests depending on it (sequential) the parent request will
* contain a RequestTree itself.
*
* @return \Symfony\Component\HttpFoundation\Request[]
* The list of requests.
*/
public
function
getRequests
()
{
return
$this
->
requests
;
}
/**
* Is this tree the base one?
*
* @return bool
* TRUE if the tree is for the master request.
*/
public
function
isRoot
()
{
return
!
$this
->
getParentId
();
}
/**
* Get the parent ID of the request this tree belongs to.
*
* @return string
*/
public
function
getParentId
()
{
return
$this
->
parentId
;
}
/**
* Find all the sub-trees in this tree.
*
* @return static[]
* An array of trees.
*/
public
function
getSubTrees
()
{
$trees
=
array_map
(
function
(
Request
$request
)
{
return
$request
->
attributes
->
get
(
static
::
SUBREQUEST_TREE
);
},
$this
->
getRequests
());
return
array_filter
(
$trees
);
}
/**
* Find a request in a tree based on the request ID.
*
* @param string $request_id
* The unique ID of a request in the blueprint to find in this tree.
*
* @return \Symfony\Component\HttpFoundation\Request|NULL $request
* The request if found. NULL if not found.
*/
public
function
getDescendant
(
$request_id
)
{
// Search this level's requests.
$found
=
array_filter
(
$this
->
getRequests
(),
function
(
Request
$request
)
use
(
$request_id
)
{
return
$request
->
attributes
->
get
(
static
::
SUBREQUEST_ID
)
==
$request_id
;
});
if
(
count
(
$found
))
{
return
reset
(
$found
);
}
// If the request is not in this level, then traverse the children's trees.
$found
=
array_filter
(
$this
->
getRequests
(),
function
(
Request
$request
)
use
(
$request_id
)
{
/** @var static $sub_tree */
if
(
!
$sub_tree
=
$request
->
attributes
->
get
(
static
::
SUBREQUEST_TREE
))
{
return
FALSE
;
}
return
$sub_tree
->
getDescendant
(
$request_id
);
});
if
(
count
(
$found
))
{
return
reset
(
$found
);
}
return
NULL
;
}
/**
* Is the request tree done?
*
* @return bool
* TRUE if all the requests in the tree and it's descendants are done.
*/
public
function
isDone
()
{
// The tree is done if all of the requests and their children are done.
return
array_reduce
(
$this
->
getRequests
(),
function
(
$is_done
,
Request
$request
)
{
return
$is_done
&&
static
::
isRequestDone
(
$request
);
},
TRUE
);
}
/**
* Check if a request and all its possible children are done.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return bool
* TRUE if is done. FALSE otherwise.
*/
protected
static
function
isRequestDone
(
Request
$request
)
{
// If one request is not done, the whole tree is not done.
if
(
!
$request
->
attributes
->
get
(
static
::
SUBREQUEST_DONE
))
{
return
FALSE
;
}
// If the request has children, then make sure those are done too.
/** @var static $sub_tree */
if
(
$sub_tree
=
$request
->
attributes
->
get
(
static
::
SUBREQUEST_TREE
))
{
if
(
!
$sub_tree
->
isDone
())
{
return
FALSE
;
}
}
return
TRUE
;
}
}
src/Controller/FrontController.php
0 → 100644
View file @
09ef18a4
<?php
namespace
Drupal\subrequests\Controller
;
use
Drupal\Core\Controller\ControllerBase
;
use
Drupal\subrequests\Blueprint\Parser
;
use
Drupal\subrequests\Blueprint\RequestTree
;
use
Symfony\Component\DependencyInjection\ContainerInterface
;
use
Symfony\Component\HttpFoundation\Request
;
use
Symfony\Component\HttpKernel\HttpKernelInterface
;
class
FrontController
extends
ControllerBase
{
/**
* @var \Drupal\subrequests\Blueprint\Parser
*/
protected
$parser
;
/**
* @var \Symfony\Component\HttpKernel\HttpKernelInterface
*/
protected
$httpKernel
;
/**
* FrontController constructor.
*/
public
function
__construct
(
Parser
$parser
,
HttpKernelInterface
$http_kernel
)
{
$this
->
parser
=
$parser
;
$this
->
httpKernel
=
$http_kernel
;
}
/**
* {@inheritdoc}
*/
public
static
function
create
(
ContainerInterface
$container
)
{
return
new
static
(
$container
->
get
(
'subrequests.blueprint_parser'
),
$container
->
get
(
'http_kernel'
)
);
}
/**
* Controller handler.
*/
public
function
handle
(
Request
$request
)
{
$this
->
parser
->
parseRequest
(
$request
);
$responses
=
[];
/** @var \Drupal\subrequests\Blueprint\RequestTree $tree */
$root_tree
=
$request
->
attributes
->
get
(
RequestTree
::
SUBREQUEST_TREE
);
$trees
=
[
$root_tree
];
// Handle all the sub-requests.
while
(
!
$root_tree
->
isDone
())
{
// Get all the requests in the trees for the previous pass.
$requests
=
array_reduce
(
$trees
,
function
(
array
$carry
,
RequestTree
$tree
)
{
return
array_merge
(
$carry
,
$tree
->
getRequests
());
},
[]);
// Get the next batch of trees for the next level.
$trees
=
array_reduce
(
$trees
,
function
(
array
$carry
,
RequestTree
$tree
)
{
return
array_merge
(
$carry
,
$tree
->
getSubTrees
());
},
[]);
// Handle the requests for the trees at this level and gather the
// responses.
$level_responses
=
array_map
(
array
(
$this
->
httpKernel
,
'handle'
,
),
$requests
);
$responses
=
array_merge
(
$responses
,
$level_responses
);
}
return
$this
->
parser
->
combineResponses
(
$responses
);
}
}
src/EventSubscriber/SubresponseSubscriber.php
0 → 100644
View file @
09ef18a4
<?php
namespace
Drupal\subrequests\EventSubscriber
;
use
Drupal\subrequests\Blueprint\RequestTree
;
use
Symfony\Component\EventDispatcher\EventSubscriberInterface
;
use
Symfony\Component\HttpKernel\Event\FilterResponseEvent
;
use
Symfony\Component\HttpKernel\KernelEvents
;
class
SubresponseSubscriber
implements
EventSubscriberInterface
{
/**
* Marks the request as done.
*
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
* The event to process.
*/
public
function
onResponse
(
FilterResponseEvent
$event
)
{
$request
=
$event
->
getRequest
();
$request
->
attributes
->
set
(
RequestTree
::
SUBREQUEST_DONE
,
TRUE
);
}
/**
* {@inheritdoc}
*/
public
static
function
getSubscribedEvents
()
{
// Run shortly before \Drupal\Core\EventSubscriber\FinishResponseSubscriber.
$events
[
KernelEvents
::
RESPONSE
][]
=
[
'onResponse'
,
5
];
return
$events
;
}
}
src/Normalizer/JsonBlueprintDenormalizer.php
0 → 100644
View file @
09ef18a4
<?php
namespace
Drupal\subrequests\Normalizer
;
use
Drupal\subrequests\Blueprint\RequestTree
;
use
Symfony\Component\HttpFoundation\Request
;
use
Symfony\Component\Serializer\Normalizer\DenormalizerInterface
;
use
Symfony\Component\Serializer\SerializerAwareInterface
;
use
Symfony\Component\Serializer\SerializerInterface
;
use
Symfony\Component\Serializer\Serializer
;
class
JsonBlueprintDenormalizer
implements
DenormalizerInterface
,
SerializerAwareInterface
{
/**
* @var \Symfony\Component\Serializer\Serializer
*/
protected
$serializer
;
/**
* {@inheritdoc}
*/
public
function
setSerializer
(
SerializerInterface
$serializer
)
{
if
(
!
is_a
(
$serializer
,
Serializer
::
class
))
{
throw
new
\
ErrorException
(
'Serializer is unable to normalize or denormalize.'
);
}
$this
->
serializer
=
$serializer
;
}
/**
* {@inheritdoc}
*/
public
function
denormalize
(
$data
,
$class
,
$format
=
NULL
,
array
$context
=
array
())
{
// The top level is an array of normalized requests.
$requests
=
array_map
(
function
(
$item
)
use
(
$format
)
{
return
$this
->
serializer
->
denormalize
(
$item
,
Request
::
class
,
$format
);
},
$data
);
return
new
RequestTree
(
$requests
);
}
/**
* {@inheritdoc}
*/
public
function
supportsDenormalization
(
$data
,
$type
,
$format
=
NULL
)
{
return
$format
===
'json'
&&
$type
===
RequestTree
::
class
&&
is_array
(
$data
)
&&
!
static
::
arrayIsKeyed
(
$data
);
}
/**
* Check if an array is keyed.
*
* @param array $input
* The input array to check.
*
* @return bool
* True if the array is keyed.
*/
public
static
function
arrayIsKeyed
(
array
$input
)
{
$keys
=
array_keys
(
$input
);
// If the array does not start at 0, it is not numeric.
if
(
$keys
[
0
]
!==
0
)
{
return
TRUE
;
}
// If there is a non-numeric key, the array is not numeric.
$numeric_keys
=
array_filter
(
$keys
,
'is_numeric'
);
if
(
count
(
$keys
)
!=
count
(
$numeric_keys
))
{
return
TRUE
;
}
// If the keys are not following the natural numbers sequence, then it is
// not numeric.
for
(
$index
=
1
;
$index
<
count
(
$keys
);
$index
++
)
{
if
(
$keys
[
$index
]
-
$keys
[
$index
-
1
]
!==
1
)
{
return
TRUE
;
}
}
return
FALSE
;
}
}
src/Normalizer/JsonSubrequestDenormalizer.php
0 → 100644
View file @
09ef18a4
<?php
namespace
Drupal\subrequests\Normalizer
;
use
Drupal\subrequests\Blueprint\Parser
;
use
Symfony\Component\HttpFoundation\HeaderBag
;
use
Symfony\Component\HttpFoundation\Request
;
use
Symfony\Component\Serializer\Normalizer\DenormalizerInterface
;
use
Drupal\Component\Utility\NestedArray
;
class
JsonSubrequestDenormalizer
implements
DenormalizerInterface
{
/**
* Denormalizes data back into an object of the given class.
*
* @param mixed $data data to restore
* @param string $class the expected class to instantiate
* @param string $format format the given data was extracted from
* @param array $context options available to the denormalizer
*
* @return object
*/
public
function
denormalize
(
$data
,
$class
,
$format
=
NULL
,
array
$context
=
array
())
{
if
(
!
Parser
::
isValidSubrequest
(
$data
))
{
throw
new
\
RuntimeException
(
'The provided blueprint contains an invalid subrequest.'
);
}
$data
[
'path'
]
=
parse_url
(
$data
[
'path'
],
PHP_URL_PATH
);
if
(
!
is_array
(
$data
[
'query'
]))
{
$query
=
array
();
parse_str
(
$data
[
'query'
],
$query
);
$data
[
'query'
]
=
$query
;
}
$data
=
NestedArray
::
mergeDeep
(
$data
,
array
(
'query'
=>
array
(),
'body'
=>
array
(),
'headers'
=>
array
(),
),
parse_url
(
$data
[
'path'
]));
/** @var \Symfony\Component\HttpFoundation\Request $master_request */
$master_request
=
$context
[
'master_request'
];
$request
=
Request
::
create
(
$data
[
'path'
],
static
::
getMethodFromAction
(
$data
[
'action'
]),
empty
(
$data
[
'body'
])
?
$data
[
'query'
]
:
$data
[
'body'
],
$master_request
->
cookies
,
$master_request
->
files
,
$master_request
->
server
,
NULL
);
// Maintain the same session as in the master request.
$request
->
setSession
(
$master_request
->
getSession
());
// Replace the headers by the ones in the subrequest.
$request
->
headers
=
new
HeaderBag
(
$data
[
'headers'
]);
// Add the content ID to the sub-request.
$content_id
=
empty
(
$data
[
'requestId'
])
?
md5
(
serialize
(
$data
))
:
$data
[
'requestId'
];
$request
->
headers
->
add
([
'Content-ID'
,
[
'<'
.
$content_id
.
'>'
]]);
return
$request
;
}
/**
* Checks whether the given class is supported for denormalization by this
* normalizer.
*
* @param mixed $data Data to denormalize from
* @param string $type The class to which the data should be denormalized
* @param string $format The format being deserialized from
*
* @return bool
*/
public
function
supportsDenormalization
(
$data
,
$type
,
$format
=
NULL
)
{
return
$format
===
'json'
&&
$type
===
Request
::
class
&&
is_array
(
$data
)
&&
JsonBlueprintDenormalizer
::
arrayIsKeyed
(
$data
);
}
/**
* Gets the HTTP method from the list of allowed actions.
*
* @param string $action
* The action name.
*
* @return string
* The HTTP method.
*/
public
static
function
getMethodFromAction
(
$action
)
{
switch
(
$action
)
{
case
'create'
:
return
Request
::
METHOD_POST
;
case
'update'
:
return
Request
::
METHOD_PATCH
;
case
'replace'
:
return
Request
::
METHOD_PUT
;
case
'delete'
:
return
Request
::
METHOD_DELETE
;
case
'exists'
:
return
Request
::
METHOD_HEAD
;
case
'discover'
:
return
Request
::
METHOD_OPTIONS
;
default
:
return
Request
::
METHOD_GET
;
}
}
}
src/Normalizer/MultiresponseNormalizer.php
0 → 100644
View file @
09ef18a4
<?php
namespace
Drupal\subrequests\Normalizer
;
use
Symfony\Component\HttpFoundation\Response
;
use
Symfony\Component\Serializer\Normalizer\NormalizerInterface
;
use
Symfony\Component\Serializer\Normalizer\scalar
;
class
MultiresponseNormalizer
implements
NormalizerInterface
{
/**
* {@inheritdoc}
*/
public
function
normalize
(
$object
,
$format
=
NULL
,
array
$context
=
array
())
{
$delimiter
=
$context
[
'delimiter'
];
$separator
=
sprintf
(
"
\r\n
--%s
\r\n
"
,
$delimiter
);
// Join the content responses with the separator.
$content_items
=
array_map
(
function
(
Response
$part_response
)
{
return
sprintf
(
"%s
\r\n\r\n
%s"
,
$part_response
->
headers
,
$part_response
->
getContent
()
);
},
(
array
)
$object
);
return
sprintf
(
"--%s
\r\n
"
,
$delimiter
)
.
implode
(
$separator
,
$content_items
)
.
sprintf
(
"
\r\n
--%s--"
,
$delimiter
);
}
/**
* {@inheritdoc}
*/
public
function
supportsNormalization
(
$data
,
$format
=
NULL
)
{
if
(
$format
!==
'multipart-response'
)
{
return
FALSE
;
}
if
(
!
is_array
(
$data
))
{
return
FALSE
;
}
$responses
=
array_filter
(
$data
,
function
(
$response
)
{
return
$response
instanceof
Response
;
});
if
(
count
(
$responses
)
!==
count
(
$data
))
{
return
FALSE
;
}
return
TRUE
;
}
}
subrequests.info.yml
0 → 100644
View file @
09ef18a4
name
:
Subrequests
type
:
module
description
:
'
Add
a
front
controller
that
you
can
use
to
make
subrequests.'
core
:
8.x
subrequests.permissions.yml
0 → 100644
View file @
09ef18a4
issue subrequests
:
title
:
'
Issue
subrequests'
description
:
'
Allow
using
the
subrequests
front
controller
to
respond
to
multiple
requests.'
subrequests.routing.yml
0 → 100644
View file @
09ef18a4
subrequests.front-controller
:
path
:
'
/subrequests'
defaults
:
_title
:
'
Returns
the
trace
recorded
by
test
proxy
session
handlers
as
JSON'
_controller
:
'
\Drupal\subrequests\Controller\FrontController::handle'
methods
:
[
POST
]