Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
project
subrequests
Commits
6aa75661
Unverified
Commit
6aa75661
authored
Sep 12, 2017
by
Mateu Aguiló Bosch
Browse files
feat(Misc): Rewrite to bring it closer to the nodejs module
parent
49a2161e
Changes
10
Hide whitespace changes
Inline
Side-by-side
src/Blueprint/
Pars
er.php
→
src/Blueprint/
BlueprintManag
er.php
View file @
6aa75661
<?php
namespace
Drupal\subrequests\Blueprint
;
use
Drupal\Core\Cache\CacheableResponse
;
use
Drupal\Core\Cache\CacheableResponseInterface
;
use
Drupal\subrequests\SubrequestsTree
;
use
Symfony\Component\HttpFoundation\Request
;
use
Symfony\Component\HttpFoundation\Response
;
use
Symfony\Component\Serializer\Serializer
Interface
;
use
Symfony\Component\Serializer\Serializer
;
/**
* 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
{
class
BlueprintManager
{
/**
* The deserializer.
*
* @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
)
{
public
function
__construct
(
Serializer
$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.
* Takes the user input and returns a subrequest tree ready for execution.
*
* @param string $input
* The input from the user.
*
* @return \Drupal\subrequests\SubrequestsTree
*/
public
function
parseRequest
(
Request
$request
)
{
$data
=
''
;
if
(
$request
->
getMethod
()
===
Request
::
METHOD_POST
)
{
$data
=
$request
->
getContent
();
}
else
if
(
$request
->
getMethod
()
===
Request
::
METHOD_GET
)
{
$data
=
$request
->
query
->
get
(
'query'
,
''
);
}
$tree
=
$this
->
serializer
->
deserialize
(
$data
,
RequestTree
::
class
,
$request
->
getRequestFormat
(),
[
'master_request'
=>
$request
]
);
$request
->
attributes
->
set
(
RequestTree
::
SUBREQUEST_TREE
,
$tree
);
// It assumed that all subrequests use the same Mime-Type.
$this
->
type
=
$request
->
getMimeType
(
$request
->
getRequestFormat
());
public
function
parse
(
$input
,
Request
$request
)
{
/** @var \Drupal\subrequests\SubrequestsTree $output */
$output
=
$this
->
serializer
->
deserialize
(
$input
,
SubrequestsTree
::
class
,
'json'
);
$output
->
setMasterRequest
(
$request
);
return
$output
;
}
/**
...
...
@@ -76,7 +52,7 @@ class Parser {
$content_type
=
sprintf
(
'multipart/related; boundary="%s", type=%s'
,
$delimiter
,
$this
->
type
$this
->
negotiateSubContentType
(
$responses
)
);
$headers
=
[
'Content-Type'
=>
$content_type
];
...
...
@@ -96,17 +72,30 @@ class Parser {
}
/**
*
Validates if a request can be constituted from this payload
.
*
Negotiates the sub Content-Type
.
*
*
@param array $data
*
The user data representing a sub-request
.
*
Checks if all responses have the same Content-Type header. If they do, then
*
it returns that one. If not, it defaults to 'application/json'
.
*
* @return bool
* TRUE if the data is valid. FALSE otherwise.
* @param \Symfony\Component\HttpFoundation\Response[] $responses
* The responses.
*
* @return string
* The collective content type. 'application/json' if no conciliation is
* possible.
*/
public
static
function
isValidSubrequest
(
array
$data
)
{
// TODO: Implement this!
return
(
bool
)
$data
;
protected
function
negotiateSubContentType
(
$responses
)
{
$output
=
array_reduce
(
$responses
,
function
(
$carry
,
Response
$response
)
{
$ct
=
$response
->
headers
->
get
(
'Content-Type'
);
if
(
!
isset
(
$carry
))
{
$carry
=
$ct
;
}
if
(
$carry
!==
$ct
)
{
$carry
=
'application/json'
;
}
return
$carry
;
});
return
$output
?:
'application/json'
;
}
}
src/Blueprint/RequestTree.php
deleted
100644 → 0
View file @
49a2161e
<?php
namespace
Drupal\subrequests\Blueprint
;
use
Rs\Json\Pointer
;
use
Rs\Json\Pointer\NonexistentValueReferencedException
;
use
Symfony\Component\HttpFoundation\Request
;
use
Symfony\Component\HttpFoundation\Response
;
/**
* Contains the hierarchical information of the requests.
*/
class
RequestTree
{
const
ROOT_TREE_ID
=
'#ROOT#'
;
const
SUBREQUEST_TREE
=
'_subrequests_tree_object'
;
const
SUBREQUEST_ID
=
'_subrequests_content_id'
;
const
SUBREQUEST_PARENT_ID
=
'_subrequests_parent_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
);
}
/**
* Resolves the JSON Pointer references.
*
* @todo For now we are forcing the use of JSON Pointer as the only format to
* reference properties in existing responses. Allow pluggability, this step
* should probably be better placed in the subrequest normalizer.
*
* @param \Symfony\Component\HttpFoundation\Response[] $responses
* Previous responses available.
*/
public
function
dereference
(
array
$responses
)
{
$this
->
requests
=
array_map
(
function
(
Request
$request
)
use
(
$responses
)
{
$id
=
$request
->
attributes
->
get
(
static
::
SUBREQUEST_ID
);
$parent_id
=
$request
->
attributes
->
get
(
static
::
SUBREQUEST_PARENT_ID
);
// Allow replacement tokens in:
// 1. The body.
// 2. The path.
// 3. The query string values.
$content
=
$request
->
getContent
();
$changes
=
static
::
replaceAllOccurrences
(
$responses
,
$content
);
$uri
=
$request
->
getUri
();
$changes
+=
static
::
replaceAllOccurrences
(
$responses
,
$uri
);
foreach
(
$request
->
query
as
$key
=>
$value
)
{
$new_key
=
$key
;
$query_changes
=
static
::
replaceAllOccurrences
(
$responses
,
$new_key
);
$query_changes
+=
static
::
replaceAllOccurrences
(
$responses
,
$value
);
if
(
$query_changes
)
{
$request
->
query
->
remove
(
$key
);
$request
->
query
->
set
(
$new_key
,
$value
);
}
}
// If there is anything to update.
if
(
$changes
)
{
// We need to duplicate the request to force recomputing the internal
// caches.
$request
=
static
::
cloneRequest
(
$request
,
$uri
,
$content
,
$id
,
$parent_id
);
}
return
$request
;
},
$this
->
getRequests
());
}
/**
* 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
;
}
/**
* Do in-place replacements for an input string containing replacement tokens.
*
* @param array $responses
* The pool of responses where to find the replacement data.
* @param string $input
* The string containing the token to replace. It's passed by reference and
* modified if necessary.
*
* @return int
* The number of replacements made.
*/
public
static
function
replaceAllOccurrences
(
array
$responses
,
&
$input
)
{
if
(
is_array
(
$input
))
{
$changes
=
0
;
// Apply the replacement recursively on the array keys and values.
foreach
(
$input
as
$key
=>
$value
)
{
$new_key
=
$key
;
$local_changes
=
static
::
replaceAllOccurrences
(
$responses
,
$new_key
);
$local_changes
+=
static
::
replaceAllOccurrences
(
$responses
,
$value
);
$changes
+=
$local_changes
;
if
(
$local_changes
)
{
unset
(
$input
[
$key
]);
$input
[
$new_key
]
=
$value
;
}
}
return
$changes
;
}
// Detect {{/foo#/bar}}
$pattern
=
'/\{\{\/([^\{\}]+)@(\/[^\{\}]+)\}\}/'
;
$matches
=
[];
if
(
!
preg_match_all
(
$pattern
,
$input
,
$matches
))
{
return
0
;
}
for
(
$index
=
0
;
$index
<
count
(
$matches
[
1
]);
$index
++
)
{
$replacement
=
static
::
findReplacement
(
$responses
,
$matches
[
1
][
$index
],
$matches
[
2
][
$index
]
);
$pattern
=
sprintf
(
'/%s/'
,
preg_quote
(
$matches
[
0
][
$index
],
'/'
));
$input
=
preg_replace
(
$pattern
,
$replacement
,
$input
);
}
return
$index
;
}
/**
* Find a replacement in the responses for the JSON pointer.
*
* @param \Symfony\Component\HttpFoundation\Response[] $responses
* The array of responses to look data into.
* @param string $id
* The response ID to extract data from.
* @param $json_pointer_path
* The JSON pointer path of the data to extract.
*
* @throws \Rs\Json\Pointer\NonexistentValueReferencedException
* When the referenced response was not found.
*
* @return mixed
* The contents of the pointed JSON property.
*/
protected
static
function
findReplacement
(
array
$responses
,
$id
,
$json_pointer_path
)
{
$found
=
array_filter
(
$responses
,
function
(
Response
$response
)
use
(
$id
)
{
return
$response
->
headers
->
get
(
'Content-ID'
)
===
sprintf
(
'<%s>'
,
$id
);
});
$response
=
reset
(
$found
);
if
(
!
$response
instanceof
Response
)
{
throw
new
NonexistentValueReferencedException
(
'Response is still not ready.'
);
}
// Find the data in the response output.
$pointer
=
new
Pointer
(
$response
->
getContent
());
return
$pointer
->
get
(
$json_pointer_path
);
}
/**
* Clones a request and modifies certain parameters.
*
* We need to do this to reset some of the internal request caches. There may
* be a better way of doing this, but I could not find it in the time that I
* expected.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The original request.
* @param $uri
* The (potentially) new URI.
* @param $content
* The (potentially) new body content.
* @param $id
* The subrequest id.
* @param $parent_id
* The subrequest id of the parent.
*
* @return \Symfony\Component\HttpFoundation\Request
* The cloned request.
*/
protected
static
function
cloneRequest
(
Request
$request
,
$uri
,
$content
,
$id
,
$parent_id
)
{
$request
->
server
->
set
(
'REQUEST_URI'
,
$uri
);
$sub_tree
=
$request
->
attributes
->
get
(
static
::
SUBREQUEST_TREE
);
$session
=
$request
->
getSession
();
$new_request
=
Request
::
create
(
$uri
,
$request
->
getMethod
(),
(
array
)
$request
->
query
->
getIterator
(),
(
array
)
$request
->
cookies
->
getIterator
(),
(
array
)
$request
->
files
->
getIterator
(),
(
array
)
$request
->
server
->
getIterator
(),
$content
);
// Set the sub-request headers.
foreach
(
$request
->
headers
as
$key
=>
$val
)
{
$new_request
->
headers
->
set
(
$key
,
$val
);
}
$new_request
->
headers
->
set
(
'Content-ID'
,
sprintf
(
'<%s>'
,
$id
));
$new_request
->
attributes
->
set
(
static
::
SUBREQUEST_PARENT_ID
,
$parent_id
);
$new_request
->
attributes
->
set
(
static
::
SUBREQUEST_ID
,
$id
);
$new_request
->
attributes
->
set
(
static
::
SUBREQUEST_TREE
,
$sub_tree
);
$new_request
->
setSession
(
$session
);
return
$new_request
;
}
}
src/Controller/FrontController.php
View file @
6aa75661
<?php
namespace
Drupal\subrequests\Controller
;
use
Drupal\Core\Controller\ControllerBase
;
use
Drupal\subrequests\Blueprint\BlueprintManager
;
use
Drupal\subrequests\Blueprint\Parser
;
use
Drupal\subrequests\Blueprint\RequestTree
;
use
Drupal\subrequests\SubrequestsManager
;
use
Symfony\Component\DependencyInjection\ContainerInterface
;
use
Symfony\Component\HttpFoundation\Request
;
use
Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent
;
use
Symfony\Component\HttpKernel\HttpKernelInterface
;
/**
* Front controller to process Subrequests requests.
*/
class
FrontController
extends
ControllerBase
{
/**
* @var \Drupal\subrequests\Blueprint\
Pars
er
* @var \Drupal\subrequests\Blueprint\
BlueprintManag
er
*/
protected
$
pars
er
;
protected
$
blueprintManag
er
;
/**
* @var \
Symfony\Component\HttpKernel\HttpKernelInterface
* @var \
Drupal\subrequests\SubrequestsManager
*/
protected
$
httpKernel
;
protected
$
subrequestsManager
;
/**
* FrontController constructor.
*/
public
function
__construct
(
Parser
$parser
,
HttpKernelInterface
$http_kernel
)
{
$this
->
parser
=
$pars
er
;
$this
->
httpKernel
=
$http_kernel
;
public
function
__construct
(
BlueprintManager
$blueprint_manager
,
SubrequestsManager
$subrequests_manager
)
{
$this
->
blueprintManager
=
$blueprint_manag
er
;
$this
->
subrequestsManager
=
$subrequests_manager
;
}
/**
* {@inheritdoc}
*/
public
static
function
create
(
ContainerInterface
$container
)
{
return
new
static
(
$container
->
get
(
'subrequests.blueprint_
pars
er'
),
$container
->
get
(
'
http_kernel
'
)
$container
->
get
(
'subrequests.blueprint_
manag
er'
),
$container
->
get
(
'
subrequests.subrequests_manager
'
)
);
}
...
...
@@ -46,46 +48,16 @@ class FrontController extends ControllerBase {
* 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
())
{
// Requests in the current level may have references to older responses.
// This step resolves those.
array_walk
(
$trees
,
function
(
RequestTree
$tree
)
use
(
$responses
)
{
$tree
->
dereference
(
$responses
);
});
// 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
(
function
(
Request
$request
)
{
$response
=
$this
->
httpKernel
->
handle
(
$request
,
HttpKernelInterface
::
MASTER_REQUEST
);
// Manually mark the request as done. We cannot use a response
// subscriber, since it may not fire if the subrequest is cached by
// PageCache.
$request
->
attributes
->
set
(
RequestTree
::
SUBREQUEST_DONE
,
TRUE
);
$id
=
$request
->
headers
->
get
(
'Content-ID'
);
$response
->
headers
->
set
(
'Content-ID'
,
$id
);
return
$response
;
},
$requests
);
$responses
=
array_merge
(
$responses
,
$level_responses
);
$data
=
''
;
if
(
$request
->
getMethod
()
===
Request
::
METHOD_POST
)
{
$data
=
$request
->
getContent
();
}
return
$this
->
parser
->
combineResponses
(
$responses
);
elseif
(
$request
->
getMethod
()
===
Request
::
METHOD_GET
)
{
$data
=
$request
->
query
->
get
(
'query'
,
''
);
}
$tree
=
$this
->
blueprintManager
->
parse
(
$data
,
$request
);
$responses
=
$this
->
subrequestsManager
->
request
(
$tree
);
return
$this
->
blueprintManager
->
combineResponses
(
$responses
);
}
}
src/JsonPathReplacer.php
0 → 100644
View file @
6aa75661
<?php
namespace
Drupal\subrequests
;
use
JsonPath\JsonObject
;
use
Drupal\Component\Serialization\Json
;
use
Symfony\Component\HttpFoundation\Response
;
use
Symfony\Component\HttpKernel\Exception\BadRequestHttpException
;
class
JsonPathReplacer
{
/**
* Performs the JSON Path replacements in the whole batch.
*
* @param \Drupal\subrequests\Subrequest[] $batch
* The subrequests that contain replacement tokens.
* @param \Symfony\Component\HttpFoundation\Response[] $responses
* The accumulated responses from previous requests.
*
* @return \Drupal\subrequests\Subrequest[]
* An array of subrequests. Note that one input subrequest can generate N
* output subrequests. This is because JSON path expressinos can return
* multiple values.
*/
public
function
replaceBatch
(
array
$batch
,
array
$responses
)
{
return
array_reduce
(
$batch
,
function
(
array
$carry
,
Subrequest
$subrequest
)
use
(
$responses
)
{
return
array_merge
(
$carry
,
$this
->
replaceItem
(
$subrequest
,
$responses
)
);
},
[]);
}
/**
* Searches for JSONPath tokens in the request and replaces it with the values
* from previous responses.
*
* @param \Drupal\subrequests\Subrequest $subrequest
* The list of requests that can contain tokens.
* @param \Symfony\Component\HttpFoundation\Response[] $pool
* The pool of responses that can content the values to replace.
*
* @returns \Drupal\subrequests\Subrequest[]
* The new list of requests. Note that if a JSONPath token yields many
* values then several replaced subrequests will be generated from the input
* subrequest.
*/
protected
function
replaceItem
(
Subrequest
$subrequest
,
array
$pool
)
{