A modest collection of PHP libraries used at SparkFun.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

588 lines
16 KiB

<?php
namespace SparkLib\Application;
use \SparkLib\Application;
use \SparkLib\Application\Responder;
use \SparkLib\Application\Redirect;
use \SparkLib\Application\Action;
use \SparkLib\Fail;
use \SparkLib\Template;
use \SparkLib\Renderable;
use \Exception;
/**
* Child classes should provide public methods corresponding
* to actions.
*
* For now, see comments in Application.
*
* @author Brennen Bearnes <brennen@sparkfun.com>
*/
abstract class Controller {
protected $_app;
protected $_action = null;
protected $_errors = array();
protected $_layout = null;
protected $_defaultLayout = 'default.tpl.php';
protected $_template = null;
protected $_mime = 'text/html';
protected $_type = 'text/html';
protected $_headers = array(
'X-UA-Compatible: chrome=1', // adds support for Google Chrome Frame for IE - if we recommend it we need this
'X-Dinosaur-Says: RAWR',
);
protected $_defaultExtension = 'html';
protected $_dbiWrite = true;
protected $_modelInfo = array();
public static $useacl = false; // should this controller use the ACL to glean perms automatically?
/**
* @param \SparkLib\Application that's running the show.
*/
public function __construct (Application $app)
{
$this->_app = $app;
$this->postConstruct();
}
/**
* To be overloaded in child classes for doing constructor-like things;
* called in the constructor.
*
* Not called init() because I didn't want to eat any more of the available
* namespace for actions.
*
* You might, for example, set up a default responder here to be
* overridden later.
*/
protected function postConstruct () { }
/**
* Fire off an action (a public method on this controller), and do
* something appropriate with the results.
*
* $this->action() may return:
*
* A string or integer, to be printed.
* An array, to be encoded as JSON and printed.
* A Template, to be rendered and printed.
* Anything which implements SparkLib\Application\Action, which will be "fired".
* - for an example, see SparkLib\Redirect.
*
* Ideally, no output will be sent to the client outside of this method.
*
* @param $action string method to fire
* @return void
*/
public function handleResult ($result)
{
if ((! $result) && $this->hasResponse()) {
// the method didn't return anything, but it did set up a responder
// object with one or more callback functions for different content
// types
$wanted_type = $this->req()->mapType();
// fall back to defaults if we don't have a responder function for this.
// will be an extension, not a mime type.
if (! $this->_response->__isset($wanted_type))
$wanted_type = $this->_defaultExtension;
$result = $this->_response->run($wanted_type);
$this->setType(Application::$extensionToMime[$wanted_type]);
}
// Handle resources (file handles)
if ($result && is_resource($result)){
// Examine the resource to set the appropriate type.
switch (get_resource_type($result)){
case 'gd':
$this->setType('image/png');
break;
default:
$this->setType(false);
throw new Exception("unsupported resource type [" . get_resource_type($result) . "] from {$this->_action}()");
break;
}
}
// Send headers - passed off to environment for SAPI-specific
// handling
foreach ($this->_headers as $header) {
$this->_app->env()->header($header);
}
$this->_app->env()->header('Content-Type: ' . $this->getType());
// If this is a HEAD request, we don't want a response body
if ($this->req() instanceof \SparkLib\Application\Request\Head)
return;
// Render result based on type
if (is_string($result) || is_int($result)) echo $result;
elseif (is_array($result)) echo $this->jsonPrep($result);
elseif ($result instanceof Renderable) echo $result->render();
elseif ($result instanceof Action) $result->fire();
elseif (is_resource($result) && $this->getType()) {
switch ($this->getType()){
case 'image/png':
imagepng($result);
break;
default:
throw new Exception("unsupported mime type [{$this->getType()}] from {$this->_action}()");
break;
}
} elseif ($result instanceof \Generator) {
foreach ($result as $yielded) {
echo $yielded;
}
} else {
throw new Exception(
"expected string, array, resource, generator, Template, or Action from {$this->_action}(), but got a " . gettype($result)
);
}
}
/**
* Do json_encode() and make sure that SparkLib\Fail won't tack anything
* on to the response. This is something of a hack for the case where
* we haven't set up a respondTo()->json callback but are still returning
* an array to be turned into JSON from an action, and should only really
* matter in the development environment.
*
* TODO: Get better at setting response types based on the kind of thing
* we are actually sending back. Also, get better at using respondTo() for
* things where we need something other than text/html. - BPB
*
* @param $result array to be converted into JSON
*/
protected function jsonPrep (array &$result)
{
Fail::$errorLogAll = true;
return json_encode($result);
}
/**
* Get the controlling app for this controller
*
* @return \SparkLib\Application
*/
protected function app () { return $this->_app; }
/**
* Return the controller app's Request object, which should model the
* current GET or POST.
*
* @return SparkLib\Application\Request
*/
protected function req () { return $this->_app->req(); }
/**
* Return the controller appl's Route object, which should model the
* current route.
*/
protected function route () { return $this->_app->route(); }
/**
* Access the current template for this application/controller/action.
*
* @return \SparkLib\Template
*/
protected function template ()
{
if (! $this->_template)
$this->setTemplate();
return $this->_template;
}
/**
* Public version of template().
*
* @return \SparkLib\Template
*/
public function getTemplate ()
{
return $this->template();
}
/**
* Set the current template to either the default or a specified
* alternative.
*
* @param optional string template filename
*/
protected function setTemplate ($template = null)
{
if (null === $template) {
$template = $this->_app->action() . '.tpl.php';
} elseif (is_string($template) && (! strpos($template, '.'))) {
// bare string name, append ".tpl.php"
$template .= '.tpl.php';
}
$tpl_dir = strtolower(get_class($this->_app)) . '/' . $this->_app->controller();
if ($template instanceof Template) {
$this->_template = $template;
} else {
// new template with app & controller available:
$this->_template = new Template(
null,
array('app' => $this->_app, 'ctl' => $this)
);
$this->_template->setTemplateDirRel($tpl_dir);
$this->_template->setTemplate($template);
}
return $this;
}
/**
* Access the current page layout for this application.
*
* @return Template for current layout
*/
protected function layout ()
{
if (! $this->_layout)
$this->setLayout();
return $this->_layout;
}
/**
* Public version of layout().
*
* @return \SparkLib\Template
*/
public function getLayout ()
{
return $this->layout();
}
/**
* Set the current page layout to either the default or a specified
* alternative.
*
* @param $layout_file string optional template filename
*/
protected function setLayout ($layout_file = null)
{
if (! $layout_file)
$layout_file = $this->_defaultLayout;
if (! strpos($layout_file, '.'))
$layout_file .= '.tpl.php';
$this->_layout = new Template(
null,
['app' => $this->_app,
'ctl' => $this]
);
$this->_layout->setTemplateDirRel(strtolower(get_class($this->_app)) . '/layouts');
$this->_layout->setTemplate($layout_file);
return $this;
}
/**
* Set a Content-Type
*
* @param $type string content type
* @return string content type
*/
protected function setType($type)
{
if ($type !== 'text/html')
Fail::$errorLogAll = true; // suppress some pesky HTML generation
$this->_type = $type;
return $type;
}
/**
* Reroute to a different action and return its results.
*
* As a side effect, resets the action on the current Application.
*
* TODO: Something about this is fucked. The app and the controller
* seem like they're the wrong kind of coupled now.
*
* @param string name of action to reroute to
* @return array|string|int|SparkLib\Application\Action result of re-routed action
*/
protected function reroute ($action)
{
$this->_app->setAction($action);
return $this->$action();
}
/**
* Access a Responder for this controller. Responder __set
* magic will treat properties as, more or less, filetype extensions
* corresponding to an appropriate MIME type, and expects them to be
* set to an anonymous function which returns appropriate data.
*
* There are two viable syntaxes for setting up a response:
*
* <code>
* // some action:
* public function view ()
* {
* $somedino = new FooSaurus(123);
* // interesting things expected to happen here
*
* $this->respondTo(array(
* 'xml' => function () use ($somedino) {
* return $somedino->toXml(); // yeah, this needs to be written too
* },
*
* 'jpeg' => function () use ($somedino) {
* // should headers be set here, or should that be automatic?
* return $somedino->renderJpeg(); // mostly I'm kidding
* }
* ));
*
* // notice that you don't need to return anything from the action here.
* }
* </code>
*
* Or...
*
* <code>
* public function view ()
* {
* // ...
*
* $this->respondTo()->xml = function () use ($somedino) {
* };
*
* // etc.
* }
* </code>
*/
protected function respondTo ($responses = null)
{
if (! isset($this->_response)) {
if (! isset($responses))
$responses = array();
$this->_response = new Responder($responses); // I know, I know, autovivification is scary
} elseif (is_array($responses)) {
$this->_response = new Responder($responses);
}
return $this->_response;
}
protected function hasResponse ()
{
return isset($this->_response);
}
/**
* Get a Redirect that goes back to the index with request params in p
* This is used by the login stuff that currently still lives in the main layout
* template.
*
* TODO: untangle.
*/
protected function goHome()
{
$app = $this->app();
$getstring = $app->controller() . '/' . $app->action() . $this->req()->compact();
return new \SparkLib\Application\Redirect($app->url() . '?p=' . urlencode($getstring), 401);
}
/**
* @return string current Content-Type
*/
public function getType()
{
return $this->_type;
}
/**
* Set a custom HTTP header
*
* @param string http header
* @return string http header
*/
protected function addHttpHeader ($header)
{
$this->_headers[] = $header;
return $header;
}
/**
* Attempt to create a dinosaur.
* Uses the request object as well as an optional array of presets for values
*/
protected function createModel ($presets = array())
{
if (! isset($this->_modelInfo['class'])) {
throw new Exception('Insufficient info for magick create');
}
$classname = $this->_modelInfo['class'];
if (! class_exists($classname)) {
throw new UnexpectedValueException("{$classname} is not a valid class");
}
$instance = new $classname;
$user = $_SESSION['user']->adminUser();
$instance->modificationInfo(array('user' => $user->getId()));
$pass_messages = ($this->req()->mapType() == 'html');
// Build the $source array of values for the new record by starting with the request
// object and adding key/val pairs from the presets array, if present.
// Do not allow a value $presets to overwrite a value in the request object.
$source = $this->req()->getArray();
foreach ($presets as $field => $value) {
if (! isset($source[$field]))
$source[$field] = $value;
}
try {
$instance->setFrom($source);
$instance->insert();
if ($pass_messages)
$this->_app->passMessage('Created', 'success');
return $instance;
}
catch (SparkRecordException $e) {
if ($pass_messages)
$this->_app->passMessage('Create failed', 'error');
return false;
}
}
/**
* Attempt to update an associated dinosaur.
*/
protected function updateModel ()
{
if (! isset($this->_modelInfo['class'])) {
throw new Exception('Insufficient info for magick update');
}
$classname = $this->_modelInfo['class'];
if (! class_exists($classname)) {
throw new UnexpectedValueException("{$classname} is not a valid class");
}
$req = $this->req()->getArray();
$user = $_SESSION['user']->adminUser();
$instance = new $classname($this->req()->id);
if (is_array($this->_modelInfo['perms'])) {
foreach ($this->_modelInfo['perms'] as $field => $perm) {
if (isset($req[$field]) && ! $user->hasPermission(key($perm), current($perm)) &&
// xxx this is duplicating some logic in dino&update, asking if the
// field will change. i don't like it, but i don't know how
// else to stop this from throwing false warnings.
$req[$field] != $instance->$field
) {
$this->_app->passMessage("You are not allowed to update {$field}", 'error');
unset($req[$field]);
}
}
}
// use loose comparison here to deal with the fact that we're getting
// strings but the models have actual typed values (we don't want to set
// identical values to the ones that're already set):
$fields = array_keys($req);
foreach ($fields as $field) {
if (! $instance->isValidField($field))
continue;
if ($req[$field] == $instance->$field) {
unset($req[$field]);
}
}
$instance->setFrom($req);
$instance->modificationInfo(array('user' => $user->getId()));
$pass_messages = ($this->req()->mapType() == 'html');
try {
if ($instance->update()) {
if ($pass_messages)
$this->_app->passMessage('Updated', 'success');
}
else {
if ($pass_messages)
$this->_app->passMessage('Nothing to update', 'message');
}
return $instance;
}
catch (SparkRecordException $e) {
if ($pass_messages)
$this->_app->passMessage('Update failed', 'error');
return false;
}
}
/**
* Return a specific module/action
*/
public function getModule ($module)
{
if(! class_exists('\Spark\Module\\' . $module))
throw new Exception('Invalid Module: ' . $module);
$class = '\Spark\Module\\' . $module;
$module = new $class(array(
'ctl' => $this,
'app' => $this->_app
));
return $module;
}
/**
* Get the current HTTP headers
*
* @return array http headers
*/
public function getHttpHeaders ()
{
return $this->_headers;
}
/*
* Return a Link for a given action on the current controller.
* Defaults to the bare controller, which means action will be routed to index.
*
* TODO: Make this actually respect the current controller (this class) instead
* of what Application thinks is the current controller... This is too
* tightly coupled.
*
* @param $action string optional action name (uses default otherwise)
* @return SparkLib\Application\Link
*/
public function linkAction ($action = null)
{
$link = $this->_app->link($this->_app->controller());
if ($action)
$link->action($action);
return $link;
}
}