neoform / neoform
PHP MVC Toolbox
dev-master
2019-03-27 06:25 UTC
Requires
- php: >=5.6
Suggests
- ext-curl: *
This package is not auto-updated.
Last update: 2024-05-17 20:24:41 UTC
README
This is a toolbox I've developed over the years that has been used for many websites I've built. The primary mission I had when creating this library was to make the code fast and east to use. The main features of this toolbox:
- MVC and Bootstrapping:
Request\Request
(immutable object composed of the HTTP request data)- Routed via a
Router\Routes
map (cached for performance) Neoform\Router\Controller
then generates aRender\View
object (eg, HTML, JSON, or Raw)Response\Response
object is passed the rendered View- Bootstrap decides what to do with the Response, usually echoing it
- Service provider tools via
Service\Service
, allowing for resource based objects to only be activated when needed. This helps greatly when doing dependency injection whilst avoiding unnecessary resource allocation (eg, opening a MySQL or Redis connection). - Session handling via
Session\Auth
that connects with a powerful ACL (Acl
) that allows for user control via roles and groups. - Entity mapping from a data source automatically gets cached at the
record level. Multiple different types of cache can be employed. Unlike
most entity caching systems, this one can use Memcache/Redis (or others)
and will never serve stale cache.
- Loading a
Entity\Model
is as easy as:Neoform\User\Model::fromPk(591);
. If the record does not exist in cache, it will be fetched and saved to cache for every subsequent lookup. - Loading an
Entity\Collection
has the same feature set:Neoform\User\Collection::byStatus(2);
. The result will be cached, and the cache will be deleted as soon as any Model in the Collection has changed.
- Loading a
- Configs that are object based located in an
Config\Environment
file, which is subsequently cached for performance. Instead of just dumping a bunch of random array values into a big messy config file, configs are validated twice before being compiled/cached. This means you can always trust the values found in the config file, even if it's been inherited multiple times.- Sad Face:
echo isset($config['core']['site_name']) ? $config['core']['site_name'] : 'Oops';
- Happy Face:
echo Neoform\Core\Config::get()->getSiteName();
.
- Sad Face:
- Input Validation is done via an extremely flexible Model/Collection
design pattern that allows for
Input\Validation
objects to be created for various scenarios, and applied to theInput\Collection
. If any value has an error, or is absent whilst listed as required, anInput\Error\Collection
will be made available. - Auto-Generation of
Entity
classes based on MySQL/PostgreSQL table mappings. Tables, Fields, Indexes and Foreign keys are used to map out the relationships betweenEntity
objects. Subdivided into two main groups (Entity\Records
andEntity\Links
) you can interact with the Entities in a very natural way, and the code is all generated for you with a simple CLI script.
Composer Install
If you wish you can install PHP Neoform via Composer.
Add neoform/neoform
as a dependency in your project's composer.json
file:
{ "require": { "neoform/neoform": "dev-master" } }
Examples
Sample Index:
<?php // Include the framework's core require_once(realpath(__DIR__ . "/../library/Neoform/Core.php")); $core = Neoform\Core::build( __DIR__ . '/..', 'MyApp\Environment\\ProductionEnvironment' ); // Create bootstrap for the MVC $bootstrap = new MyApp\Bootstrap( (isset($_SERVER['REQUEST_URI']) ? rawurldecode($_SERVER['REQUEST_URI']) : '/'), $_GET, $_POST, $_FILES, $_SERVER, $_COOKIE ); $bootstrap ->buildResponse() ->render();
Sample Bootstrap:
<?php namespace MyApp; use Neoform; /* * Make a website - standard/simple bootstrap */ class Bootstrap { /** * @var Neoform\Request\Builder */ private $requestBuilder; /** * Create bootstrap application * * @param string $routerPath * @param array $get * @param array $post * @param array $files * @param array $server * @param array $cookies */ public function __construct($routerPath, array $get=[], array $post=[], array $files=[], array $server=[], array $cookies=[]) { $this->requestBuilder = (new Neoform\Request\Builder(Neoform\Router\Config::get())) ->setPath( $routerPath, Neoform\Locale\Config::get() ) ->setGet($get) ->setPost($post) ->setFiles($files) ->setServer($server) ->setCookies($cookies) ->loadSession(); } /** * Executes the bootstrap * When buffer output is turned off, all content and headers generated by views are sent straight to the browser * * @return Neoform\Response\Response */ public function buildResponse() { try { // Get locale from URL $locale = Neoform\Locale::getService()->get(); if ($localeIso2 = $this->requestBuilder->getPath()->getSlugs()->getLocaleIso2()) { $locale->set($localeIso2); } // Get routes $dao = new Neoform\Router\Route\Dao( Neoform\Core::get()->getCachePath(), Neoform\Router\Config::get() ); // Get routes based on locale $routeInfo = $dao->get($locale->get()); $locale->set_routes($routeInfo['routes']); // Router Builder $routerBuilder = new Neoform\Router\Builder( $this->requestBuilder, Neoform\Router\Config::get(), $routeInfo['controllers'] ); // Build the router $router = $routerBuilder->build(); // Apply controller/non-controller slugs to request builder $this->requestBuilder->applyRouter($router); // Build the response based on the router's signals return $router->buildResponse( $this->requestBuilder->build(), new Neoform\Response\Http\Builder ); } catch (Neoform\Router\Exception $e) { switch ((int) $e->getCode()) { case 401: // Login required // Bounce back to current URL after login return $this->requireLogin($this->requestBuilder->build(), $e->getMessage()); case 403: // Access denied $response = new Neoform\Response\Http\Builder; $response->setView( (new Controller\Error($this->requestBuilder->build(), $response))->action403() ); return $response->build(); case 404: // Not found $response = new Neoform\Response\Http\Builder; $response->setView( (new Controller\Error($this->requestBuilder->build(), $response))->action404() ); return $response->build(); case 500: // Server error Neoform\Error\Lib::log($e); $response = new Neoform\Response\Http\Builder; $response->setView( (new Controller\Error($this->requestBuilder->build(), $response))->action500() ); return $response->build(); default: $response = new Neoform\Response\Http\Builder; $response->setView( (new Controller\Error($this->requestBuilder->build(), $response)) ->actionGeneric((int) $e->getCode(), $e->getMessage()) ); return $response->build(); } // Model Exception } catch (Neoform\Entity\Exception $e) { $response = new Neoform\Response\Http\Builder; $response->setView( (new Controller\Error($this->requestBuilder->build(), $response)) ->actionGeneric(404, $e->getMessage(), $e->getDescription()) ); return $response->build(); // All other exceptions } catch (\Exception $e) { Neoform\Error\Lib::log($e); $response = new Neoform\Response\Http\Builder; $response->setView((new Controller\Error($this->requestBuilder->build(), $response))->action500()); return $response->build(); } } /** * @param Neoform\Request\Model $request * @param string|null $message * * @return Neoform\Response\Response */ private function requireLogin(Neoform\Request\Model $request, $message=null) { $request->getSession()->getFlash()->set('login_bounce', $request->getServer()->getUri()); if ($message) { $request->getSession()->getFlash()->set('login_message', $message); } if ($request->getServer()->getAccepts()->accepts('application/json')) { return $this->jsonStatusResponse('login'); } return Neoform\Response\Http\Builder::redirect( "{$request->getBaseUrl()->getSecureBaseUrl()}/account/login" ); } /** * @param string $status * * @return Neoform\Response\Http */ private function jsonStatusResponse($status) { $response = new Neoform\Response\Http\Builder; $json = new Neoform\Render\Json; $json->set('status', $status); $response->setView($json, 'application/json'); return $response->build(); } }
Sample Controller:
<?php namespace MyApp\Controller; use Neoform; use MyApp; class Info extends MyApp\Controller { public function defaultAction() { $view = new Neoform\Render\Html; $this->applyDefaults($view); if ($this->request->getGet()->count() || $this->request->getParameters()->count()) { return $this->show404(); } $view->slug = $slug = (string) $this->request->getNonControllerSlugs()->get('slug'); $validPages = [ 'about' => 'About Us', 'merchants' => 'Merchants', 'advertising' => 'Advertising', 'partnerships' => 'Partnerships', 'syndication' => 'Syndication', 'marketing' => 'Marketing', 'privacy' => 'Privacy Policy', 'tos' => 'Terms of Service', 'contact' => 'Contact', ]; if (! isset($validPages[$slug])) { return $this->show404(); } $view->metaTitle = "{$validPages[$slug]} - Example.ca"; $view->applyTemplate('info'); $this->response->setView($view); } }
Sample Router Mapping
<?php namespace MyApp; use Neoform\Router\Route\Model as Route; use Neoform; class Routes extends Neoform\Router\Routes { /** * @return Route */ public function get() { return new Route([ 'controller' => 'MyApp\Controller\Index', 'children' => [ // API 'api' => new Route([ 'controller' => 'MyApp\Controller\Api', 'secure' => true, 'slugs' => [ 0 => 'action', ], ]), // Account 'account' => new Route([ 'controller' => 'MyApp\Controller\Account', 'secure' => true, 'locale' => [ 'en' => 'account', 'fr' => 'compte', ], 'children' => [ 'login' => new Route([ 'controller' => 'MyApp\Controller\Account\Login', 'locale' => [ 'en' => 'login', 'fr' => 'connexion', ], ]), ], ]), ], ]); } }
Sample Environment (Configs):
<?php namespace MyApp\Environment; use MyApp; use Neoform; class Production extends Neoform\Config\Environment { public function getName() { return 'Production'; } protected function definitions() { // Core $this->merge(new Neoform\Core\Config\Builder([ 'site_name' => 'Example.com', 'default_error_controller' => 'MyApp\Controller\Error', 'default_error_controller_action' => 'action500', ])); // Router $this->merge(new Neoform\Router\Config\Builder([ 'domain' => 'example.com', 'https' => [ 'regular' => true, 'secure' => true, ], // Required subdomains - default 'subdomain_default' => [ 'regular' => 'www', 'secure' => 'www', ], // CDN base URL 'cdn' => 'cdn.example.com', // Routing map 'routes_map_class' => 'MyApp\Routes', ])); // Cookies (default values) $this->merge(new Neoform\Request\Parameters\Cookies\Config\Builder); // Sessions $this->merge(new Neoform\Session\Config\Builder([ // random string to make the ref code more random - you can change this, but it will // kill all sessions (forms that are being filled out). 'xsrf_salt' => 'abcdefghijklmnopqrstuvwxyz1234567890.,/!@#$%^&*()*', // Session handlers 'flash_cache_engine' => 'Neoform\Redis', // Which server is used when reading 'flash_cache_pool_read' => 'master', // Which server is used when writing 'flash_cache_pool_write' => 'master', ])); } }
Questions/Comments
Since this is a pet project of mine and the toolbox is relatively simple, if you need any help with it, feel free to contact me at: ian@oshaughnessy.cc