Responses
In the previous chapter we already stated
a request comes in, is processed, and a response is sent back.
For CMSimple_XH plugins, these reponses are often just strings,
either returned from a plugin call function, or appended to the output ($o
)
in several other cases.
However, sometimes they are not strings.
We have already seen an example in the previous cahpter,
but let us consider the following excerpt from Michael Svarrer's gallery plugin
(version 0.9, admin.php
):
if($gallery){
// code omitted
$o.=print_plugin_admin('on');
// code omitted
if($action=='upload'){
// code omitted
header("Location: ".$sn."?&gallery&admin=plugin_main&path=".$path."&action=view&error=".$error);exit;
}
// code omitted
}
This code is executed outside of any function, and the omitted code uses an awful lot of global variables. This is generally bad practice, so let us rewrite to use a function:
if ($gallery) {
$o .= process_gallery();
}
function process_gallery()
{
global $action, $sn, …;
// code omitted
$o .= print_plugin_admin('on');
// code omitted
if ($action == 'upload') {
// code omitted
header("Location: ".$sn."?&gallery&admin=plugin_main&path=".$path."&action=view&error=".$error);exit;
}
return $o;
}
On $action=='upload'
we end script execution without returning anything.
But in some other cases, the process_gallery()
function would actually return a string.
In the type system of PHP the return value of process_gallery()
would be string|never
as of PHP 8.1,
or for older version the slightly weaker string|void
.
While this is not a particular problem per se,
Plib_XH offers a nice generalization, namely Response
.
So with Plib_XH we can rewrite process_gallery()
:
function process_gallery(): Response
{
global $action, $sn, …;
// code omitted
$o .= print_plugin_admin('on');
// code omitted
if ($action == 'upload') {
// code omitted
$url = $request->url()->with("admin", "plugin_main")->with("path", $path)
->with("action", "view")->with("error", $error);
return Response::redirect($url->absolute());
}
return Response::create($o);
}
So instead of returning string|never
,
we now always return a Response
object,
and declare the return type of the function accordingly.
This expresses our intent, enables PHP to report any violation of this contract
(e.g. when we forget to return a Response
from some code path).
And it may even help PHP to optimize the code execution.
Response::create()
creates a response with a string which will be output;
Response::redirect()
creates a response which will trigger a redirect.
Note that process_gallery()
is now free of side-effects
(the exit
statement is gone), which is a useful property of functions,
which we are going to exploit in the next chapter.
However, at some point these side-effects have to occur;
otherwise the plugin could not fulfill its purpose.
This is done by invoking (aka. triggering) the Response
in the global
scope:
if ($gallery) {
$response = process_gallery();
$o .= $response();
}
This is an explicit version, to make it easier to understand what is going on. Usually, you want to use the short version, though:
if ($gallery) {;
$o .= process_gallery()();
}
That is, call the function, and then immediately invoke the returned Response
.
Now let us reconsider the gblist()
function of Qualifire's guestbook plugin
(version 08-beta, index.php
):
function gblist($gb_filename) {
// code omitted
if (!$fp = fopen($gbfile, 'a+')) {
$t .= "<br><hr>ERROR: Cannot open file ($gbfile)<hr><br>";
} else {
// code omitted
header("Location: http://" . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'] . "?" . $_SERVER['QUERY_STRING']); exit;
}
// code omitted
return $t;
}
We may want to use Response
s here, too, but since gblist()
is a function implementing a plugin call, we have to return a string
.
A simple and straight forward solution is to extract an internal
function:
/** @return string|never */
function gblist(string $gb_filename)
{
return gllist_internal($gb_filename)();
}
function gblist_internal(string $gb_filename): Response
{
// code omitted
if (!$fp = fopen($gbfile, 'a+')) {
// $view has been created in the omitted code above
$t .= $view->message("fail", "error_open", $gbfile);
} else {
// code omitted
// $request has been created in the omitted code above
return Response::redirect($request->url()->absolute());
}
// code omitted
return Response::create($t);
}
We also use View::message()
(which wraps XH_message
) for brevity
and internationalization purposes.
Anyhow, given that
a request comes in, is processed, and a response is sent back.
we may want to make that explicit:
/** @return string|never */
function gblist(string $gb_filename)
{
return gllist_internal(Request::current(), $gb_filename)();
}
function gblist_internal(Request $request, string $gb_filename): Response
{
// code omitted
if (!$fp = fopen($gbfile, 'a+')) {
// $view has been created in the omitted code above
$t .= $view->message("fail", "error_open", $gbfile);
} else {
// code omitted
return Response::redirect($request->url()->absolute());
}
// code omitted
return Response::create($t);
}