diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f9902fb..ae183033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,50 @@ # Parable PHP Framework Changelog +### 1.1.0 + +__Changes__ + +- The `parable` command has been fixed up massively. There's now a `\Parable\Framework\ConsoleApp` class, which handles the actual logic. By using this, you can offer your own command instead of `parable`. +- `defines.php` now sets a global constant `APP_CONTEXT` to either `web` or `cli`. This can help you figure out what context you're running in. +- `defines.php` now also defines a new function: `register_parable_package()`. This can be used by external Parable Packages to register themselves with Parable at the soonest possible moment. See below for details. +- `\Parable\Console\App` now also adds the command you set through `setDefaultCommand()`. It's now also possible to `removeCommandByName()`. +- `\Parable\DI\Container` gained `getDependenciesFor()` so it's possible to get just an array of instantiated dependencies. +- `\Parable\Framework\App` gained multiple hooks: `HOOK_LOAD_CONFIG_BEFORE`/`AFTER`, `HOOK_LOAD_INITS_BEFORE`/`AFTER`, `HOOK_LOAD_LAYOUT_BEFORE`/`AFTER`. +- `\Parable\Framework\App` now supports layouts, which are loaded just before the response is sent. See the `Response` changes below for details. The config values used to load the templates are `parable.layout.header` and `parable.layout.footer`. They're expected to be `.phtml` files. +- `\Parable\Framework\Loader` has been added, containing `InitLoader` and `CommandLoader`, easing the use of either and separating that logic away nicely. +- `\Parable\Framework\Package\PackageManager` was added, allowing external packages to register themselves with Parable before Parable is actually completely set up. Packages are loaded _before_ config and anything else. They get to use the hooks mentioned above. +- `\Parable\Framework\Package\PackageInterface` is an interface used to define a Parable Package. +- `\Parable\Framework\Authentication` has gained `resetUser()` and `reset()`, which calls it and `revokeAuthentication()` both. This makes it possible to unset the user as well. +- `\Parable\Framework\Authentication` has also gained `setUserIdProperty()` and `getUserIdProperty()`, for more control over how to load users. +- `\Parable\Http\Output\OutputInterface` has gained `acceptsContent($content)`, which will return a boolean for whether it accepts a type of content or not. +- `\Parable\Http\Output\OutputInterface::prepare()` now has to return a string value. Always. Exception thrown if not. This makes Output behavior expected.D +- `\Parable\Http\Output\AbstractOutput` was added, which implements `acceptsContent()` to return default `true`. +- `\Parable\Http\Output\Html` overrides `acceptsContent()` to only accept `string` or `null` types. `Json` accepts all types. +- `\Parable\Http\Response` has gained `setHeaderContent()` and `setFooterContent`, which are pre/appended to the content when sending. This is used by `App`'s layout logic. Also there's getters for both. +- `\Parable\Http\Response` also gained `stopOutputBuffer()`, which does the same as `returnOutputBuffer()` but doesn't return anything. `stopAllOutputBuffers()` pretty much does what it says. +- `\Parable\ORM\Query`'s join methods now all accept a new optional parameter, `$tableName`. Normally, the table name is set to the table already set on the query. But now you can override it. This makes it possible to join tables with other tables, neither of which are forced to be the main table. +- `\Parable\ORM\Query` has gained `whereCondition()`, taking the standard `$key`, `$comparator` and optional `$value` (default `null`) and `$tableName`. This was added to ease adding simple wheres, without having to _always_ build a condition set. +- `\Parable\ORM\Query\ConditionSet` now accepts a 4th parameter, which is `$tableName`, in case you want to check against a different table's values. +- `\Parable\Rights\Rights` has gained `getRightsNames()`, which will return the names of all rights configured. + +__Bugfixes__ + +- `\Parable\Console\Output` has had its tags fixed up. It's now possible to combine fore- and background colors, as was always intended. Some small typo fixes in the tag names, but they're easy to fix. +- `\Parable\Framework\App` has lost some classes from its constructor. They're now loaded on an as-needed basis. So if you don't need the session, it won't be loaded, for example. +- `\Parable\Framework\App` now loads the database immediately after loading the Config, instead of much later. +- `\Parable\Framework\Dispatcher` didn't check route return values and blindly attempted to string-concatenate them. With the help of the `Output` changes, it now checks what kind of data it is and handles it accordingly. +- `\Parable\GetSet\Base` now throws an exception when `getAll()` is called for a global resource type, but the resource doesn't exist. Example case: attempting to use session data before the session is started. +- `\Parable\Http\Response` now checks whether it's possible to append output buffers to content, and uses `acceptsContent` to make sure only valid content is set using the available output method. +- `\Parable\ORM\Model` now returns all fields when `exportToArray()` is called and no `$model->exportable` values are actually available. Remember, Parable's not here to hold your hand. You're responsible for only exporting the right data! + +__Parable Packages Information__ + +Parable Packages are rather simple. Say you want to build something that relies on and extends Parable. If so, just create a class that implements `PackageInterface`, implement the methods defined there, and in your composer.json, make sure that under `autoload`/`files` it loads a php file (_after_ `defines.php` itself is loaded) that calls `register_parable_package()` (as defined in Parable's own `defines.php`) and passes the name of your package file. + +Parable will attempt to load the commands and inits defined in your parable package file, and they'll be available from the start of the application's runtime. The commands _will_ be available from the default `parable` command as well. + +More details will be added to the documentation once released. + ### 1.0.0 Considering the 0.11.x branch as Release Candidate 1 and the 0.12.x branch as RC2, it's time to ship final Parable. This release brings a major clean-up, documentation, and some useful additions. @@ -7,6 +52,7 @@ Considering the 0.11.x branch as Release Candidate 1 and the 0.12.x branch as RC If you're new to Parable, welcome! None of this is relevant for you :) __Changes__ + - All method doc blocks now have explanatory text, even if it's superfluous, for documentation purposes. - `\Parable\Console\App` now supports adding multiple commands in one go, using `addCommands([...])`. - `\Parable\Console\Command\Help` now can generate a string for the usage of a command. Try it yourself: `vendor/bin/parable help init-structure`. Usage is also added to any exception caught by `\Parable\Console\App`'s exception handler. @@ -58,6 +104,7 @@ __Changes__ - It's now possible to set a new config value - `parable.database.soft-quotes` - to either `true` (default) or `false`. If `true`, Parable will fake quotes for values if there's no database instance available. If set to `false`, it'll refuse to quote instead. __Backwards-incompatible Changes__ + - `Bootstrap.php` has been removed. `\Parable\Framework\App` handles its own setup now. This makes it easier to implement App without much hassle. - `SessionMessage` has been moved from the `GetSet` component into `Framework`, as it isn't a `GetSet` instance itself but merely uses the `Session` instance. - `\Parable\Console` no longer accepts options in the format `--option value`, but only in the following: `--option=value`. This is because if you had an option which didn't require a value, and was followed by an argument, the argument would be seen as the option's value instead. @@ -81,6 +128,7 @@ __Backwards-incompatible Changes__ - Two config keys were renamed: `parable.session.autoEnable` has become `parable.session.auto-enable` and `parable.app.homeDir` has become `parable.app.homedir`. The option for `init-structure` has also become `--homedir`. __Bugfixes__ + - `\Parable\Console\Output` had a bug where moving the cursors would mess with the functionality of `clearLine()`. Line length is no longer kept track of, but whether or not the line is clearable is a boolean value. Moving the cursor up/down or placing it disables line clearing, writing anything enables it again. When you clear the line, the line gets cleared using the terminal width. - `\Parable\Console\Parameter` had a bug where providing an argument after an option with an `=` sign in it (so `script.php command option=value arg`) would see the argument as the option value and overwrite the actual value. Fixed by @dmvdbrugge in PR #31. Thanks! - `\Parable\Console\Parameter` had a bug, where false-equivalent values passed to an option would be seen as the option being provided without a value, making the returned value `true`. Fixed by @dmvdbrugge in PR #37. Thanks! diff --git a/Makefile b/Makefile index c3615d8c..c4dd5ec7 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,13 @@ coverage: dependencies rm -rf ./coverage vendor/bin/phpunit --coverage-html ./coverage tests +tests-clean: + vendor/bin/phpunit --verbose tests + +coverage-clean: + rm -rf ./coverage + vendor/bin/phpunit --coverage-html ./coverage tests + server: @echo Running on http://127.0.0.1:5678 php -t ../../.. -S 127.0.0.1:5678 php-server.php diff --git a/parable b/parable index 922a40c6..d39f1c1e 100755 --- a/parable +++ b/parable @@ -1,47 +1,7 @@ #!/usr/bin/env php addLocation(BASEDIR . DS . 'app'); -$autoloader->register(); - -// Set the basedir on the path -$path = \Parable\DI\Container::create(\Parable\Filesystem\Path::class); -$path->setBaseDir(BASEDIR); - -$app->setName("Parable " . \Parable\Framework\App::PARABLE_VERSION); - -$helpCommand = \Parable\DI\Container::get(\Parable\Console\Command\Help::class); -$initStructureCommand = \Parable\DI\Container::get(\Parable\Framework\Command\InitStructure::class); - -// Always add Help & Init -$app->addCommands([ - $helpCommand, - $initStructureCommand, -]); - -// Attempt to load commands set by the user -if (file_exists($path->getDir('app'))) { - /** @var \Parable\Framework\Config $config */ - $config = \Parable\DI\Container::get(\Parable\Framework\Config::class); - $config->load(); - - if ($config->get('parable.commands')) { - // We don't try/catch because the dev shouldn't add non-existing classes. - foreach ($config->get('parable.commands') as $commandClassName) { - $app->addCommand(\Parable\DI\Container::get($commandClassName)); - } - } -} - -$app->setDefaultCommand($helpCommand); -$app->run(); +$consoleApp = \Parable\DI\Container::get(\Parable\Framework\ConsoleApp::class); +$consoleApp->setErrorReportingEnabled(true); +$consoleApp->run(); diff --git a/phpunit.xml b/phpunit.xml index b56f36a9..9fd7e5a6 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -15,7 +15,7 @@ ./src/ - ./src/Framework/Bootstrap.php + ./src/defines.php ./src/Console/Input.php ./src/GetSet/Session.php ./src/Framework/Command/InitStructure.php diff --git a/src/Console/App.php b/src/Console/App.php index d1039b78..531257ed 100644 --- a/src/Console/App.php +++ b/src/Console/App.php @@ -39,10 +39,12 @@ public function __construct( set_exception_handler(function ($e) { // @codeCoverageIgnoreStart + + /** @var \Exception $e */ $this->output->writeErrorBlock($e->getMessage()); if ($this->activeCommand) { - $this->output->writeln("Usage: " . $this->activeCommand->getUsage()); + $this->output->writeln('Usage: ' . $this->activeCommand->getUsage()); } // @codeCoverageIgnoreEnd }); @@ -109,12 +111,13 @@ public function addCommands(array $commands) */ public function setDefaultCommandByName($commandName) { - $this->defaultCommand = $commandName; + $this->defaultCommand = $commandName; return $this; } /** - * Set the default command to use if no command is given. + * Set the default command to use if no command is given. Also + * adds the command. * * @param \Parable\Console\Command $command * @@ -122,6 +125,7 @@ public function setDefaultCommandByName($commandName) */ public function setDefaultCommand(\Parable\Console\Command $command) { + $this->addCommand($command); $this->setDefaultCommandByName($command->getName()); return $this; } @@ -149,6 +153,18 @@ public function shouldOnlyUseDefaultCommand() return $this->onlyUseDefaultCommand; } + /** + * Returns whether the $commandName is registered. + * + * @param string $commandName + * + * @return bool + */ + public function hasCommand($commandName) + { + return isset($this->commands[$commandName]); + } + /** * Return the command by name if it's set on the application. * @@ -158,7 +174,7 @@ public function shouldOnlyUseDefaultCommand() */ public function getCommand($commandName) { - if (isset($this->commands[$commandName])) { + if ($this->hasCommand($commandName)) { return $this->commands[$commandName]; } return null; @@ -174,6 +190,21 @@ public function getCommands() return $this->commands; } + /** + * Remove a command by name. + * + * @param string $commandName + * + * @return $this + */ + public function removeCommandByName($commandName) + { + if ($this->hasCommand($commandName)) { + unset($this->commands[$commandName]); + } + return $this; + } + /** * Run the application. * diff --git a/src/Console/Command.php b/src/Console/Command.php index fa6e9296..b1979351 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -196,7 +196,7 @@ public function getUsage() if ($argument->isRequired()) { $string[] = $argument->getName(); } else { - $string[] = "[" . $argument->getName() . "]"; + $string[] = "[{$argument->getName()}]"; } } @@ -209,7 +209,7 @@ public function getUsage() $string[] = "[--{$optionString}]"; } - return implode(" ", $string); + return implode(' ', $string); } /** diff --git a/src/Console/Command/Help.php b/src/Console/Command/Help.php index 5c8068f3..d2f68129 100644 --- a/src/Console/Command/Help.php +++ b/src/Console/Command/Help.php @@ -12,7 +12,7 @@ class Help extends \Parable\Console\Command public function __construct() { - $this->addArgument("command_name"); + $this->addArgument('command_name'); } /** @@ -27,9 +27,9 @@ public function run() $this->output->newline(); } - $commandName = $this->parameter->getArgument("command_name"); + $commandName = $this->parameter->getArgument('command_name'); if ($commandName) { - $this->showCommandHelp($this->parameter->getArgument("command_name")); + $this->showCommandHelp($this->parameter->getArgument('command_name')); } else { $this->showGeneralHelp(); } diff --git a/src/Console/Input.php b/src/Console/Input.php index e09c3281..f02ecc68 100644 --- a/src/Console/Input.php +++ b/src/Console/Input.php @@ -135,7 +135,6 @@ public function disableShowInput() * Request input from the user, while hiding the actual input. Use this to request passwords, for example. * * @return string - * * @throws \Parable\Console\Exception */ public function getHidden() @@ -165,14 +164,14 @@ public function getYesNo($default = true) $value = strtolower($this->get()); // Y/N values are ALWAYS directly returned as true/false - if ($value == 'y') { + if ($value === 'y') { return true; - } elseif ($value == 'n') { + } elseif ($value === 'n') { return false; } // If no value, we return the default value - if ($value == '') { + if (empty($value)) { return (bool)$default; } diff --git a/src/Console/Output.php b/src/Console/Output.php index 691b7734..153d1976 100644 --- a/src/Console/Output.php +++ b/src/Console/Output.php @@ -9,31 +9,49 @@ class Output /** @var array */ protected $tags = [ - /* foreground colors */ - 'default' => "\e[0m", - 'black' => "\e[0;30m", - 'red' => "\e[0;31m", - 'green' => "\e[0;32m", - 'yellow' => "\e[0;33m", - 'blue' => "\e[0;34m", - 'purple' => "\e[0;35m", - 'cyan' => "\e[0;36m", - 'white' => "\e[0;37m", - - /* background colors */ - 'black_bg' => "\e[40m", - 'red_bg' => "\e[41m", - 'green_bg' => "\e[42m", - 'yellow_bg' => "\e[43m", - 'blue_bg' => "\e[44m", - 'magenta_bg' => "\e[45m", - 'cyan_bg' => "\e[46m", - 'lightgray_bg' => "\e[47m", - - /* styles */ - 'error' => "\e[0;37m\e[41m", - 'success' => "\e[0;30m\e[42m", - 'info' => "\e[0;30m\e[43m", + // Default everything + 'default' => "\e[0m", + + // Foreground colors + 'black' => "\e[;30m", + 'red' => "\e[;31m", + 'green' => "\e[;32m", + 'yellow' => "\e[;33m", + 'blue' => "\e[;34m", + 'magenta' => "\e[;35m", + 'cyan' => "\e[;36m", + 'light_gray' => "\e[;37m", + 'dark_gray' => "\e[;90m", + 'light_red' => "\e[;91m", + 'light_green' => "\e[;92m", + 'light_yellow' => "\e[;93m", + 'light_blue' => "\e[;94m", + 'light_magenta' => "\e[;95m", + 'light_cyan' => "\e[;96m", + 'white' => "\e[;97m", + + // Background colors + 'bg_black' => "\e[40m", + 'bg_red' => "\e[41m", + 'bg_green' => "\e[42m", + 'bg_yellow' => "\e[43m", + 'bg_blue' => "\e[44m", + 'bg_magenta' => "\e[45m", + 'bg_cyan' => "\e[46m", + 'bg_light_gray' => "\e[47m", + 'bg_dark_gray' => "\e[100m", + 'bg_light_red' => "\e[101m", + 'bg_light_green' => "\e[102m", + 'bg_light_yellow' => "\e[103m", + 'bg_light_blue' => "\e[104m", + 'bg_light_magenta' => "\e[105m", + 'bg_light_cyan' => "\e[106m", + 'bg_white' => "\e[107m", + + // Combined styles + 'error' => "\e[41;37m", + 'success' => "\e[42;30m", + 'info' => "\e[43;30m", ]; /** @var bool */ @@ -66,9 +84,9 @@ public function write($string) public function getTerminalWidth() { if ($this->isInteractiveShell() - || (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && getenv("shell")) + || (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && getenv('shell')) ) { - return (int)shell_exec("tput cols"); + return (int)shell_exec('tput cols'); } return self::TERMINAL_DEFAULT_WIDTH; @@ -84,9 +102,9 @@ public function getTerminalWidth() public function getTerminalHeight() { if ($this->isInteractiveShell() - || (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && getenv("shell")) + || (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && getenv('shell')) ) { - return (int)shell_exec("tput lines"); + return (int)shell_exec('tput lines'); } return self::TERMINAL_DEFAULT_HEIGHT; @@ -101,7 +119,7 @@ public function getTerminalHeight() */ public function isInteractiveShell() { - return function_exists("posix_isatty") && posix_isatty(0); + return function_exists('posix_isatty') && posix_isatty(0); } /** @@ -276,7 +294,7 @@ public function clearLine() } $this->cursorReset(); - $this->write(str_repeat(" ", $this->getTerminalWidth())); + $this->write(str_repeat(' ', $this->getTerminalWidth())); $this->cursorReset(); $this->disableClearLine(); @@ -349,8 +367,8 @@ public function writeBlockWithTags($string, array $tags = []) { $strlen = mb_strlen($string); - $tagsOpen = ""; - $tagsClose = ""; + $tagsOpen = ''; + $tagsClose = ''; if (count($tags) > 0) { foreach ($tags as $tag) { $tagsOpen .= "<{$tag}>"; diff --git a/src/Console/Parameter.php b/src/Console/Parameter.php index ea83541d..bda65029 100644 --- a/src/Console/Parameter.php +++ b/src/Console/Parameter.php @@ -78,13 +78,13 @@ public function parseParameters() foreach ($this->parameters as $parameter) { if (substr($parameter, 0, 2) === "--") { // For options, we need to see if it has a value (x=y) or not. - $optionParts = explode("=", $parameter); + $optionParts = explode('=', $parameter); if (count($optionParts) > 1) { list($key, $value) = $optionParts; - $this->options[ltrim($key, "-")] = $value; + $this->options[ltrim($key, '-')] = $value; } else { - $this->options[ltrim($parameter, "-")] = true; + $this->options[ltrim($parameter, '-')] = true; } } else { // For arguments, we need to see if the first one is the command name or not. @@ -124,6 +124,7 @@ public function getCommandName() * @param \Parable\Console\Parameter\Option[] $options * * @return $this + * @throws \Parable\Console\Exception */ public function setCommandOptions(array $options) { @@ -200,6 +201,7 @@ public function getOptions() * @param \Parable\Console\Parameter\Argument[] $arguments * * @return $this + * @throws \Parable\Console\Exception */ public function setCommandArguments(array $arguments) { diff --git a/src/Console/Parameter/Argument.php b/src/Console/Parameter/Argument.php index 90f8987f..ae0bf63d 100644 --- a/src/Console/Parameter/Argument.php +++ b/src/Console/Parameter/Argument.php @@ -32,9 +32,12 @@ public function setRequired($required) { if (!in_array( $required, - [\Parable\Console\Parameter::PARAMETER_REQUIRED, \Parable\Console\Parameter::PARAMETER_OPTIONAL]) - ) { - throw new \Parable\Console\Exception("Required must be one of the PARAMETER_* constants."); + [ + \Parable\Console\Parameter::PARAMETER_REQUIRED, + \Parable\Console\Parameter::PARAMETER_OPTIONAL, + ] + )) { + throw new \Parable\Console\Exception('Required must be one of the PARAMETER_* constants.'); } $this->required = $required; return $this; diff --git a/src/Console/Parameter/Option.php b/src/Console/Parameter/Option.php index f6d02044..5dc3926f 100644 --- a/src/Console/Parameter/Option.php +++ b/src/Console/Parameter/Option.php @@ -29,9 +29,12 @@ public function setValueRequired($valueRequired) { if (!in_array( $valueRequired, - [\Parable\Console\Parameter::OPTION_VALUE_REQUIRED, \Parable\Console\Parameter::OPTION_VALUE_OPTIONAL]) - ) { - throw new \Parable\Console\Exception("Value required must be one of the OPTION_VALUE_* constants."); + [ + \Parable\Console\Parameter::OPTION_VALUE_REQUIRED, + \Parable\Console\Parameter::OPTION_VALUE_OPTIONAL, + ] + )) { + throw new \Parable\Console\Exception('Value required must be one of the OPTION_VALUE_* constants.'); } $this->valueRequired = $valueRequired; return $this; diff --git a/src/DI/Container.php b/src/DI/Container.php index 39538329..d72452f7 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -47,6 +47,7 @@ public static function get($className, $parentClassName = '') /** * Instantiate a class and fulfill its dependency requirements, getting dependencies rather than creating. + * This does not store the created instance in the cache. It would have to be manually stored. * * @param string $className * @param string $parentClassName @@ -88,19 +89,39 @@ protected static function createInstance($className, $parentClassName = '', $cre $className = self::cleanName($className); try { - $reflection = new \ReflectionClass($className); - } catch (\Exception $e) { - $message = "Could not create instance of '{$className}'"; + $dependencies = self::getDependenciesFor($className, $createAll); + } catch (\Parable\DI\Exception $e) { + $message = $e->getMessage(); if ($parentClassName) { $message .= ", required by '{$parentClassName}'"; } throw new \Parable\DI\Exception($message); } + return new $className(...$dependencies); + } + + /** + * Retrieve and instantiate all dependencies for the provided $className + * + * @param string $className + * @param bool $createAll + * + * @return array + * @throws \Parable\DI\Exception + */ + public static function getDependenciesFor($className, $createAll = false) + { + try { + $reflection = new \ReflectionClass($className); + } catch (\Exception $e) { + $message = "Could not create instance of '{$className}'"; + throw new \Parable\DI\Exception($message); + } $construct = $reflection->getConstructor(); if (!$construct) { - return new $className(); + return []; } $parameters = $construct->getParameters(); @@ -123,7 +144,7 @@ protected static function createInstance($className, $parentClassName = '', $cre $dependencies[] = self::get($subClassName, $className); } } - return new $className(...$dependencies); + return $dependencies; } /** @@ -162,8 +183,8 @@ public static function isStored($name) */ protected static function cleanName($name) { - if (substr($name, 0, 1) == "\\") { - $name = ltrim($name, "\\"); + if (substr($name, 0, 1) === '\\') { + $name = ltrim($name, '\\'); } return $name; } diff --git a/src/Event/Dock.php b/src/Event/Dock.php index 1d05983e..e049ed18 100644 --- a/src/Event/Dock.php +++ b/src/Event/Dock.php @@ -67,7 +67,7 @@ public function trigger($event = null, &$payload = null) // outside means like through the session or \Parable\GetSet or one of its sub-types. if ($dock['templatePath'] && file_exists($dock['templatePath'])) { ob_start(); - require($dock['templatePath']); + require $dock['templatePath']; $return = ob_get_clean(); echo $return; } diff --git a/src/Framework/App.php b/src/Framework/App.php index f0ea6119..8840feef 100644 --- a/src/Framework/App.php +++ b/src/Framework/App.php @@ -4,31 +4,39 @@ class App { - const PARABLE_VERSION = '1.0.0'; - - const HOOK_HTTP_404 = "parable_http_404"; - const HOOK_HTTP_200 = "parable_http_200"; - const HOOK_INIT_DATABASE_BEFORE = "parable_init_database_before"; - const HOOK_INIT_DATABASE_AFTER = "parable_init_database_after"; - const HOOK_LOAD_INITS_AFTER = "parable_load_inits_after"; - const HOOK_LOAD_ROUTES_BEFORE = "parable_load_routes_before"; - const HOOK_LOAD_ROUTES_NO_ROUTES_FOUND = "parable_load_routes_no_routes_found"; - const HOOK_LOAD_ROUTES_AFTER = "parable_load_routes_after"; - const HOOK_RESPONSE_SEND = "parable_response_send"; - const HOOK_ROUTE_MATCH_BEFORE = "parable_route_match_before"; - const HOOK_ROUTE_MATCH_AFTER = "parable_route_match_after"; - const HOOK_SESSION_START_BEFORE = "parable_session_start_before"; - const HOOK_SESSION_START_AFTER = "parable_session_start_after"; + const PARABLE_VERSION = '1.1.0'; + + const HOOK_HTTP_404 = 'parable_http_404'; + const HOOK_HTTP_200 = 'parable_http_200'; + const HOOK_LOAD_CONFIG_BEFORE = 'parable_load_config_before'; + const HOOK_LOAD_CONFIG_AFTER = 'parable_load_config_after'; + const HOOK_LOAD_INITS_BEFORE = 'parable_load_inits_before'; + const HOOK_LOAD_INITS_AFTER = 'parable_load_inits_after'; + const HOOK_INIT_DATABASE_BEFORE = 'parable_init_database_before'; + const HOOK_INIT_DATABASE_AFTER = 'parable_init_database_after'; + const HOOK_LOAD_LAYOUT_BEFORE = 'parable_load_layout_before'; + const HOOK_LOAD_LAYOUT_AFTER = 'parable_load_layout_after'; + const HOOK_LOAD_ROUTES_BEFORE = 'parable_load_routes_before'; + const HOOK_LOAD_ROUTES_NO_ROUTES_FOUND = 'parable_load_routes_no_routes_found'; + const HOOK_LOAD_ROUTES_AFTER = 'parable_load_routes_after'; + const HOOK_RESPONSE_SEND = 'parable_response_send'; + const HOOK_ROUTE_MATCH_BEFORE = 'parable_route_match_before'; + const HOOK_ROUTE_MATCH_AFTER = 'parable_route_match_after'; + const HOOK_SESSION_START_BEFORE = 'parable_session_start_before'; + const HOOK_SESSION_START_AFTER = 'parable_session_start_after'; /** @var \Parable\Framework\Config */ protected $config; - /** @var \Parable\Framework\Dispatcher */ - protected $dispatcher; - /** @var \Parable\Framework\Toolkit */ protected $toolkit; + /** @var \Parable\Framework\Package\PackageManager */ + protected $packageManager; + + /** @var \Parable\Framework\View */ + protected $view; + /** @var \Parable\Event\Hook */ protected $hook; @@ -44,38 +52,30 @@ class App /** @var \Parable\Http\Url */ protected $url; - /** @var \Parable\GetSet\Session */ - protected $session; - - /** @var \Parable\ORM\Database */ - protected $database; - /** @var bool */ protected $errorReportingEnabled = false; public function __construct( \Parable\Framework\Autoloader $autoloader, \Parable\Framework\Config $config, - \Parable\Framework\Dispatcher $dispatcher, \Parable\Framework\Toolkit $toolkit, + \Parable\Framework\Package\PackageManager $packageManager, + \Parable\Framework\View $view, \Parable\Event\Hook $hook, \Parable\Filesystem\Path $path, \Parable\Routing\Router $router, \Parable\Http\Response $response, - \Parable\Http\Url $url, - \Parable\GetSet\Session $session, - \Parable\ORM\Database $database + \Parable\Http\Url $url ) { - $this->config = $config; - $this->dispatcher = $dispatcher; - $this->toolkit = $toolkit; - $this->hook = $hook; - $this->path = $path; - $this->router = $router; - $this->response = $response; - $this->url = $url; - $this->session = $session; - $this->database = $database; + $this->config = $config; + $this->toolkit = $toolkit; + $this->packageManager = $packageManager; + $this->view = $view; + $this->hook = $hook; + $this->path = $path; + $this->router = $router; + $this->response = $response; + $this->url = $url; // Add the default location to the autoloader and register it $autoloader->addLocation(BASEDIR . DS . 'app'); @@ -86,38 +86,6 @@ public function __construct( $this->path->setBaseDir(BASEDIR); } - /** - * Enable error reporting, setting display_errors to on and reporting to E_ALL - * - * @return $this - */ - public function setErrorReportingEnabled($enabled) - { - ini_set("log_errors", 1); - - if ($enabled) { - ini_set("display_errors", 1); - error_reporting(E_ALL); - } else { - ini_set("display_errors", 0); - error_reporting(E_ALL | ~E_DEPRECATED); - } - - $this->errorReportingEnabled = $enabled; - - return $this; - } - - /** - * Return whether error reporting is currently enabled or not - * - * @return bool - */ - public function isErrorReportingEnabled() - { - return $this->errorReportingEnabled; - } - /** * Do all the setup and then attempt to match and dispatch the current url. * @@ -125,8 +93,15 @@ public function isErrorReportingEnabled() */ public function run() { - // Load the config - $this->config->load(); + // And now possible packages get their turn. + $this->packageManager->registerPackages(); + + $this->loadConfig(); + + // Init the database if it's configured + if ($this->config->get('parable.database.type')) { + $this->loadDatabase(); + } // Enable error reporting if debug is set to true if ($this->config->get('parable.debug') === true) { @@ -142,7 +117,7 @@ public function run() } // See if there's any inits defined in the config - if ($this->config->get("parable.inits")) { + if ($this->config->get('parable.inits')) { $this->loadInits(); } @@ -166,11 +141,6 @@ public function run() $currentUrl = $this->toolkit->getCurrentUrl(); $currentFullUrl = $this->toolkit->getCurrentUrlFull(); - // Init the database if it's configured - if ($this->config->get('parable.database.type')) { - $this->initDatabase(); - } - // And try to match the route $this->hook->trigger(self::HOOK_ROUTE_MATCH_BEFORE, $currentUrl); $route = $this->router->matchUrl($currentUrl); @@ -183,11 +153,57 @@ public function run() $this->hook->trigger(self::HOOK_HTTP_404, $currentFullUrl); } + $this->loadLayout(); + $this->hook->trigger(self::HOOK_RESPONSE_SEND); $this->response->send(); return $this; } + /** + * Return Parable's current version number. + * + * @return string + */ + public function getVersion() + { + return self::PARABLE_VERSION; + } + + /** + * Enable error reporting, setting display_errors to on and reporting to E_ALL + * + * @param bool $enabled + * + * @return $this + */ + public function setErrorReportingEnabled($enabled) + { + ini_set('log_errors', 1); + + if ($enabled) { + ini_set('display_errors', 1); + error_reporting(E_ALL); + } else { + ini_set('display_errors', 0); + error_reporting(E_ALL | ~E_DEPRECATED); + } + + $this->errorReportingEnabled = $enabled; + + return $this; + } + + /** + * Return whether error reporting is currently enabled or not + * + * @return bool + */ + public function isErrorReportingEnabled() + { + return $this->errorReportingEnabled; + } + /** * Start the session. * @@ -196,8 +212,11 @@ public function run() protected function startSession() { $this->hook->trigger(self::HOOK_SESSION_START_BEFORE); - $this->session->start(); - $this->hook->trigger(self::HOOK_SESSION_START_AFTER, $this->session); + + $session = \Parable\DI\Container::get(\Parable\GetSet\Session::class); + $session->start(); + + $this->hook->trigger(self::HOOK_SESSION_START_AFTER, $session); return $this; } @@ -205,14 +224,13 @@ protected function startSession() * Load all the routes, if possible. * * @return $this - * * @throws \Parable\Framework\Exception */ protected function loadRoutes() { $this->hook->trigger(self::HOOK_LOAD_ROUTES_BEFORE); - if ($this->config->get("parable.routes")) { - foreach ($this->config->get("parable.routes") as $routesClass) { + if ($this->config->get('parable.routes')) { + foreach ($this->config->get('parable.routes') as $routesClass) { $routes = \Parable\DI\Container::create($routesClass); if (!($routes instanceof \Parable\Framework\Routing\AbstractRouting)) { @@ -231,20 +249,30 @@ protected function loadRoutes() return $this; } + /** + * Load the config and trigger hooks. + */ + protected function loadConfig() + { + $this->hook->trigger(self::HOOK_LOAD_CONFIG_BEFORE); + $this->config->load(); + $this->hook->trigger(self::HOOK_LOAD_CONFIG_AFTER); + } + /** * Create instances of given init classes. * * @return $this - * - * @throws \Parable\Framework\Exception */ protected function loadInits() { - if ($this->config->get("parable.inits")) { - foreach ($this->config->get("parable.inits") as $initClass) { - \Parable\DI\Container::create($initClass); - } + $this->hook->trigger(self::HOOK_LOAD_INITS_BEFORE); + + if ($this->config->get('parable.inits')) { + $initLoader = \Parable\DI\Container::create(\Parable\Framework\Loader\InitLoader::class); + $initLoader->load($this->config->get('parable.inits')); } + $this->hook->trigger(self::HOOK_LOAD_INITS_AFTER); return $this; } @@ -254,39 +282,57 @@ protected function loadInits() * * @return $this */ - protected function initDatabase() + protected function loadDatabase() { $this->hook->trigger(self::HOOK_INIT_DATABASE_BEFORE); - $this->database->setConfig($this->config->get('parable.database')); + + $database = \Parable\DI\Container::get(\Parable\ORM\Database::class); + $database->setConfig($this->config->get('parable.database')); + $this->hook->trigger(self::HOOK_INIT_DATABASE_AFTER); return $this; } /** - * Dispatch the provided route. - * - * @param \Parable\Routing\Route $route + * Load the layout header/footer if configured. * * @return $this */ - protected function dispatchRoute(\Parable\Routing\Route $route) + protected function loadLayout() { - $this->response->setHttpCode(200); - $this->hook->trigger(self::HOOK_HTTP_200, $route); + $this->hook->trigger(self::HOOK_LOAD_LAYOUT_BEFORE); - $this->dispatcher->dispatch($route); + if ($this->config->get('parable.layout.header')) { + $this->response->setHeaderContent( + $this->view->partial($this->config->get('parable.layout.header')) + ); + } + if ($this->config->get('parable.layout.footer')) { + $this->response->setFooterContent( + $this->view->partial($this->config->get('parable.layout.footer')) + ); + } + $this->hook->trigger(self::HOOK_LOAD_LAYOUT_AFTER); return $this; } /** - * Return Parable's current version number. + * Dispatch the provided route. * - * @return string + * @param \Parable\Routing\Route $route + * + * @return $this */ - public function getVersion() + protected function dispatchRoute(\Parable\Routing\Route $route) { - return self::PARABLE_VERSION; + $this->response->setHttpCode(200); + $this->hook->trigger(self::HOOK_HTTP_200, $route); + + $dispatcher = \Parable\DI\Container::get(\Parable\Framework\Dispatcher::class); + $dispatcher->dispatch($route); + + return $this; } /** @@ -311,9 +357,9 @@ public function getVersion() public function multiple(array $methods, $url, $callable, $name = null, $templatePath = null) { $routeData = [ - "methods" => $methods, - "url" => $url, - "templatePath" => $templatePath, + 'methods' => $methods, + 'url' => $url, + 'templatePath' => $templatePath, ]; if (is_array($callable) @@ -321,14 +367,14 @@ public function multiple(array $methods, $url, $callable, $name = null, $templat && class_exists($callable[0]) && !(new \ReflectionMethod($callable[0], $callable[1]))->isStatic() ) { - $routeData["controller"] = $callable[0]; - $routeData["action"] = $callable[1]; + $routeData['controller'] = $callable[0]; + $routeData['action'] = $callable[1]; } else { - $routeData["callable"] = $callable; + $routeData['callable'] = $callable; } $this->router->addRouteFromArray( - $name ?: uniqid("", true), + $name ?: uniqid('', true), $routeData ); diff --git a/src/Framework/Authentication.php b/src/Framework/Authentication.php index 5bd03d79..c3963e0f 100644 --- a/src/Framework/Authentication.php +++ b/src/Framework/Authentication.php @@ -7,6 +7,9 @@ class Authentication /** @var string */ protected $userClassName = '\Model\User'; + /** @var string */ + protected $userIdProperty = 'id'; + /** @var object|null */ protected $user; @@ -74,7 +77,7 @@ public function generatePasswordHash($password) */ protected function checkAuthentication() { - $authSession = $this->session->get('auth'); + $authSession = $this->readFromSession(); if ($authSession) { if (isset($authSession['authenticated'])) { $this->setAuthenticated($authSession['authenticated']); @@ -169,6 +172,29 @@ public function getUserClassName() return $this->userClassName; } + /** + * Set the property to read to get the user id. + * + * @param string $property + * + * @return $this + */ + public function setUserIdProperty($property) + { + $this->userIdProperty = $property; + return $this; + } + + /** + * Return the property to read to get the user id. + * + * @return string + */ + public function getUserIdProperty() + { + return $this->userIdProperty; + } + /** * Set the user and check whether it's of the right type. * @@ -189,7 +215,7 @@ public function setUser($user) /** * Return the user entity, if it exists. * - * @return null + * @return mixed */ public function getUser() { @@ -209,12 +235,13 @@ public function authenticate($passwordProvided, $passwordHash) if (password_verify($passwordProvided, $passwordHash)) { $this->setAuthenticated(true); - if ($this->getUser()) { - $this->setAuthenticationData(['user_id' => $this->user->id]); + if ($this->getUser() && property_exists($this->getUser(), $this->getUserIdProperty())) { + $userId = $this->getUser()->{$this->getUserIdProperty()}; + $this->setAuthenticationData(['user_id' => $userId]); } - $this->session->set('auth', [ + $this->writeToSession([ 'authenticated' => true, - 'data' => $this->authenticationData, + 'data' => $this->getAuthenticationData(), ]); } else { $this->revokeAuthentication(); @@ -223,15 +250,72 @@ public function authenticate($passwordProvided, $passwordHash) } /** - * Revoke an existing authentication. + * Write data array to the session. + * + * @param array $data + * + * @return $this + */ + protected function writeToSession(array $data) + { + $this->session->set('auth', $data); + return $this; + } + + /** + * Return the session data, if it exists. + * + * @return array|null + */ + protected function readFromSession() + { + return $this->session->get('auth'); + } + + /** + * Clear the session data. + * + * @return $this + */ + protected function clearSession() + { + $this->session->remove('auth'); + return $this; + } + + /** + * Revoke an existing authentication and clear the session. * * @return $this */ public function revokeAuthentication() { $this->setAuthenticated(false); - $this->session->remove('auth'); + $this->clearSession(); + return $this; + } + + /** + * Reset the user currently stored and remove authentication data. + * + * @return $this + */ + public function resetUser() + { + $this->setAuthenticationData([]); + $this->user = null; + return $this; + } + /** + * Revoke authentication and reset user. + * + * @return $this + */ + public function reset() + { + $this->revokeAuthentication(); + $this->resetUser(); return $this; } } diff --git a/src/Framework/Autoloader.php b/src/Framework/Autoloader.php index 4de445ee..fedb3930 100644 --- a/src/Framework/Autoloader.php +++ b/src/Framework/Autoloader.php @@ -57,7 +57,7 @@ public function load($class) foreach ($this->getLocations() as $subPath) { $actualPath = str_replace('##replace##', $subPath, $path); if (file_exists($actualPath)) { - require_once($actualPath); + require_once $actualPath; return true; } } diff --git a/src/Framework/Command/InitStructure.php b/src/Framework/Command/InitStructure.php index 19790642..7d81310e 100644 --- a/src/Framework/Command/InitStructure.php +++ b/src/Framework/Command/InitStructure.php @@ -20,9 +20,9 @@ public function __construct( \Parable\Filesystem\Path $path ) { $this->addOption( - "homedir", + 'homedir', \Parable\Console\Parameter::OPTION_VALUE_REQUIRED, - "public" + 'public' ); $this->path = $path; @@ -39,7 +39,7 @@ public function __construct( */ public function run() { - $homedir = $this->parameter->getOption("homedir"); + $homedir = $this->parameter->getOption('homedir'); $homedir = ltrim($homedir, DS); $homedir_actual = $this->path->getDir($homedir); @@ -59,17 +59,17 @@ public function run() ]); if (file_exists($this->path->getDir('app')) && file_exists($this->path->getDir('public'))) { - $this->output->writeBlock("Note: It looks like you already have a structure initialized!", "info"); + $this->output->writeBlock('Note: It looks like you already have a structure initialized!', 'info'); } else { $this->output->newline(); } for (;;) { - $this->output->write("Do you want to continue? [y/N] "); + $this->output->write('Do you want to continue? [y/N] '); if ($this->input->getYesNo(false)) { break; } else { - $this->output->writeln(["", "You chose not to continue.", ""]); + $this->output->writeln(['', 'You chose not to continue.', '']); return $this; } } @@ -78,15 +78,15 @@ public function run() $this->output->write('Creating folder structure: '); $dirs = [ - "app", - "app/Command", - "app/Config", - "app/Controller", - "app/Init", - "app/Model", - "app/Routing", - "app/View", - "app/View/Home", + 'app', + 'app/Command', + 'app/Config', + 'app/Controller', + 'app/Init', + 'app/Model', + 'app/Routing', + 'app/View', + 'app/View/Home', $homedir, ]; @@ -169,10 +169,10 @@ public function run() $this->output->write('.'); // If the homedir isn't 'public', change the values in Config\App.php and .htaccess. - if ($homedir !== "public") { - $config = file_get_contents($this->path->getDir("app/Config/App.php")); + if ($homedir !== 'public') { + $config = file_get_contents($this->path->getDir('app/Config/App.php')); $config = str_replace('"homedir" => "public"', '"homedir" => "' . $homedir . '"', $config); - file_put_contents($this->path->getDir("app/Config/App.php"), $config); + file_put_contents($this->path->getDir('app/Config/App.php'), $config); $this->output->write('.'); $htaccess = file_get_contents($this->path->getDir('.htaccess')); @@ -181,9 +181,9 @@ public function run() } $this->output->write('.'); - $this->output->writeln(" OK"); + $this->output->writeln(' OK'); - $this->output->writeln(["", "Completed!", ""]); + $this->output->writeln(['', 'Completed!', '']); return $this; } } diff --git a/src/Framework/Config.php b/src/Framework/Config.php index e2a88abf..61631d8e 100644 --- a/src/Framework/Config.php +++ b/src/Framework/Config.php @@ -28,7 +28,7 @@ public function __construct( * @param string $className * * @return $this - * @throws Exception + * @throws \Parable\Framework\Exception */ public function setMainConfigClassName($className) { @@ -53,8 +53,8 @@ public function load() return $this; } - if ($this->get("parable.configs")) { - foreach ($this->get("parable.configs") as $configClass) { + if ($this->get('parable.configs')) { + foreach ($this->get('parable.configs') as $configClass) { $this->addConfig(\Parable\DI\Container::get($configClass)); } } diff --git a/src/Framework/ConsoleApp.php b/src/Framework/ConsoleApp.php new file mode 100644 index 00000000..67ee25c1 --- /dev/null +++ b/src/Framework/ConsoleApp.php @@ -0,0 +1,110 @@ +setErrorReportingEnabled(true); + + $this->consoleApp = $consoleApp; + + // Add the default location to the autoloader and register it + $autoloader->addLocation(BASEDIR . DS . 'app'); + $autoloader->register(); + + // And make sure $path has the proper BASEDIR + $path->setBaseDir(BASEDIR); + + // Set the appropriate name + $this->consoleApp->setName('Parable ' . \Parable\Framework\App::PARABLE_VERSION); + + // Add the init structure command + $this->consoleApp->addCommand($commandInitStructure); + + // And set help as default + $this->consoleApp->setDefaultCommand($commandHelp); + + // Attempt to work with the config, if it exists. + try { + // Attempt to load additional commands from the config + $config->load(); + + if ($config->get('parable.commands')) { + $commandLoader = \Parable\DI\Container::get(\Parable\Framework\Loader\CommandLoader::class); + $commandLoader->load($config->get('parable.commands')); + } + // Attempt to load error reporting setting from the config + if ($config->get('parable.debug') === true) { + $this->setErrorReportingEnabled(true); + } else { + $this->setErrorReportingEnabled(false); + } + } catch (\Parable\DI\Exception $exception) { // @codeCoverageIgnore + // It's fine, we don't need these. + } + + // And now possible packages get their turn. + $packageManager->registerPackages(); + } + + /** + * Enable error reporting, setting display_errors to on and reporting to E_ALL + * + * @param bool $enabled + * + * @return $this + */ + public function setErrorReportingEnabled($enabled) + { + ini_set('log_errors', 1); + + if ($enabled) { + ini_set('display_errors', 1); + error_reporting(E_ALL); + } else { + ini_set('display_errors', 0); + error_reporting(E_ALL | ~E_DEPRECATED); + } + + $this->errorReportingEnabled = $enabled; + + return $this; + } + + /** + * Return whether error reporting is currently enabled or not + * + * @return bool + */ + public function isErrorReportingEnabled() + { + return $this->errorReportingEnabled; + } + + /** + * Run the console application. + * + * @return mixed + * @throws \Parable\Console\Exception + */ + public function run() + { + return $this->consoleApp->run(); + } +} diff --git a/src/Framework/Dispatcher.php b/src/Framework/Dispatcher.php index a6a5901e..20c452e0 100644 --- a/src/Framework/Dispatcher.php +++ b/src/Framework/Dispatcher.php @@ -4,10 +4,10 @@ class Dispatcher { - const HOOK_DISPATCH_BEFORE = "parable_dispatch_before"; - const HOOK_DISPATCH_AFTER = "parable_dispatch_after"; - const HOOK_DISPATCH_TEMPLATE_BEFORE = "parable_dispatch_template_before"; - const HOOK_DISPATCH_TEMPLATE_AFTER = "parable_dispatch_template_after"; + const HOOK_DISPATCH_BEFORE = 'parable_dispatch_before'; + const HOOK_DISPATCH_AFTER = 'parable_dispatch_after'; + const HOOK_DISPATCH_TEMPLATE_BEFORE = 'parable_dispatch_template_before'; + const HOOK_DISPATCH_TEMPLATE_AFTER = 'parable_dispatch_template_after'; /** @var \Parable\Event\Hook */ protected $hook; @@ -50,8 +50,8 @@ public function dispatch(\Parable\Routing\Route $route) $this->hook->trigger(self::HOOK_DISPATCH_BEFORE, $route); $controller = null; - // Start output buffering and set $content to null - $content = null; + // Start output buffering and set $returnContent to null + $returnContent = null; $this->response->startOutputBuffer(); // Build the parameters array @@ -63,10 +63,10 @@ public function dispatch(\Parable\Routing\Route $route) // Call the relevant code if ($route->hasControllerAndAction()) { $controller = \Parable\DI\Container::get($route->getController()); - $content = $controller->{$route->getAction()}(...$parameters); + $returnContent = $controller->{$route->getAction()}(...$parameters); } elseif ($route->hasCallable()) { $callable = $route->getCallable(); - $content = $callable(...$parameters); + $returnContent = $callable(...$parameters); } $this->hook->trigger(self::HOOK_DISPATCH_TEMPLATE_BEFORE, $route); @@ -93,11 +93,27 @@ public function dispatch(\Parable\Routing\Route $route) $this->view->render(); } - // Get the output buffer content and check if $content holds anything. If so, append it to the $bufferContent - $content = $this->response->returnOutputBuffer() . $content; + // If the callback or controller/action returned content, we need to handle it + if ($returnContent) { + if ($this->response->getOutput()->acceptsContent($returnContent)) { + $this->response->setContent($returnContent); + } else { + $type = gettype($returnContent); + $output = get_class($this->response->getOutput()); + + // Stop the output buffer we've started above and throw + $this->response->stopOutputBuffer(); + throw new \Parable\Framework\Exception( + "Route returned value of type '{$type}', which output class '{$output}' cannot handle." + ); + } + } - // And append the content to the response object - $this->response->appendContent($content); + // Any rendered content was from before the returnContent was set, so we prepend it if there's any + $renderedContent = $this->response->returnOutputBuffer(); + if ($renderedContent) { + $this->response->prependContent($renderedContent); + } $this->hook->trigger(self::HOOK_DISPATCH_TEMPLATE_AFTER, $route); $this->hook->trigger(self::HOOK_DISPATCH_AFTER, $route); diff --git a/src/Framework/Loader/CommandLoader.php b/src/Framework/Loader/CommandLoader.php new file mode 100644 index 00000000..2eb47821 --- /dev/null +++ b/src/Framework/Loader/CommandLoader.php @@ -0,0 +1,32 @@ +consoleApp = $consoleApp; + } + + /** + * Add all commands passed to the console app. + * + * @param string[] $commandClasses + * + * @return $this + * @throws \Parable\DI\Exception + */ + public function load(array $commandClasses) + { + foreach ($commandClasses as $commandClass) { + $command = \Parable\DI\Container::create($commandClass); + $this->consoleApp->addCommand($command); + } + return $this; + } +} diff --git a/src/Framework/Loader/InitLoader.php b/src/Framework/Loader/InitLoader.php new file mode 100644 index 00000000..99985fbb --- /dev/null +++ b/src/Framework/Loader/InitLoader.php @@ -0,0 +1,22 @@ +config->get("parable.mail.sender")) { + if ($this->config->get('parable.mail.sender')) { try { - $sender = \Parable\DI\Container::create($this->config->get("parable.mail.sender")); + $sender = \Parable\DI\Container::create($this->config->get('parable.mail.sender')); $this->setMailSender($sender); } catch (\Exception $e) { - throw new \Parable\Framework\Exception("Invalid mail sender set in config."); + throw new \Parable\Framework\Exception('Invalid mail sender set in config.'); } } else { // Use PhPMail sender by default $this->setMailSender(new \Parable\Mail\Sender\PhpMail()); } - if ($this->config->get("parable.mail.from.email")) { + if ($this->config->get('parable.mail.from.email')) { $this->setFrom( - $this->config->get("parable.mail.from.email"), - $this->config->get("parable.mail.from.name") + $this->config->get('parable.mail.from.email'), + $this->config->get('parable.mail.from.name') ); } + + return $this; } /** diff --git a/src/Framework/Package/PackageInterface.php b/src/Framework/Package/PackageInterface.php new file mode 100644 index 00000000..b8c88f25 --- /dev/null +++ b/src/Framework/Package/PackageInterface.php @@ -0,0 +1,20 @@ +packages[] = $packageName; + return $this; + } + + /** + * Load all Commands from package. + * + * @param PackageInterface $package + * + * @return $this + * @throws \Parable\DI\Exception + */ + protected function loadCommands(PackageInterface $package) + { + if (!$package->getCommands() || APP_CONTEXT !== 'console') { + return $this; + } + + $commandLoader = \Parable\DI\Container::create(\Parable\Framework\Loader\CommandLoader::class); + $commandLoader->load($package->getCommands()); + return $this; + } + + /** + * Register all packages with Parable. + * + * @return $this + * @throws \Parable\DI\Exception + */ + public function registerPackages() + { + foreach ($this->packages as $packageName) { + $package = \Parable\DI\Container::create($packageName); + $this->loadCommands($package); + $this->loadInits($package); + } + return $this; + } + + /** + * Load all Inits from package. + * + * @param PackageInterface $package + * + * @return $this + * @throws \Parable\DI\Exception + */ + protected function loadInits(PackageInterface $package) + { + if (!$package->getInits()) { + return $this; + } + + $initLoader = \Parable\DI\Container::create(\Parable\Framework\Loader\InitLoader::class); + $initLoader->load($package->getInits()); + return $this; + } +} diff --git a/src/Framework/SessionMessage.php b/src/Framework/SessionMessage.php index 5844cc82..f0a63cd8 100644 --- a/src/Framework/SessionMessage.php +++ b/src/Framework/SessionMessage.php @@ -4,7 +4,7 @@ class SessionMessage { - const SESSION_KEY = "parable_session_messages"; + const SESSION_KEY = 'parable_session_messages'; /** @var \Parable\GetSet\Session */ protected $session; diff --git a/src/Framework/View.php b/src/Framework/View.php index 4c06ba7d..9e03ed15 100644 --- a/src/Framework/View.php +++ b/src/Framework/View.php @@ -72,11 +72,11 @@ protected function registerClassesFromMagicProperties() $magicProperties = $docComment ? explode(PHP_EOL, $docComment) : []; foreach ($magicProperties as $magicProperty) { - if (strpos($magicProperty, "@property") === false) { + if (strpos($magicProperty, '@property') === false) { continue; } - $partsString = trim(str_replace("* @property", "", $magicProperty)); + $partsString = trim(str_replace('* @property', '', $magicProperty)); $parts = explode('$', $partsString); list($className, $property) = $parts; @@ -87,15 +87,17 @@ protected function registerClassesFromMagicProperties() } /** - * Register a class with the View for ->property lazyloading. + * Register a class with the View for property lazy-loading. * * @param string $property * @param string $className + * + * @return $this */ public function registerClass($property, $className) { // Make sure the $className is prefixed with a backslash - $className = "\\" . ltrim($className, "\\"); + $className = '\\' . ltrim($className, '\\'); $this->classes[$property] = $className; return $this; @@ -150,7 +152,7 @@ protected function loadTemplatePath($templatePath) { $templatePath = $this->path->getDir($templatePath); if (file_exists($templatePath)) { - require($templatePath); + require $templatePath; } return $this; } diff --git a/src/GetSet/Base.php b/src/GetSet/Base.php index 6f8f8ac3..9c4c29ea 100644 --- a/src/GetSet/Base.php +++ b/src/GetSet/Base.php @@ -40,6 +40,7 @@ public function getResource() * Return all from resource if resource is set. * * @return array + * @throws \Parable\GetSet\Exception */ public function getAll() { @@ -49,6 +50,14 @@ public function getAll() if ($this->useLocalResource) { return $this->localResource; } + + // If we're attempting to use a global resource but it doesn't exist, we've got a problem. + if (!isset($GLOBALS[$this->getResource()])) { + throw new \Parable\GetSet\Exception( + "Attempting to use global resource '{$this->getResource()}' but resource not available." + ); + } + return $GLOBALS[$this->getResource()]; } @@ -66,7 +75,7 @@ public function get($key, $default = null) { $resource = $this->getAll(); - $keys = explode(".", $key); + $keys = explode('.', $key); foreach ($keys as $key) { if (!isset($resource[$key])) { $resource = $default; @@ -128,7 +137,7 @@ public function count() */ public function set($key, $value) { - $keys = explode(".", $key); + $keys = explode('.', $key); $data = $this->getAll(); @@ -191,7 +200,7 @@ public function setAll(array $values) */ public function remove($key) { - $keys = explode(".", $key); + $keys = explode('.', $key); $data = $this->getAll(); diff --git a/src/Http/Output/AbstractOutput.php b/src/Http/Output/AbstractOutput.php new file mode 100644 index 00000000..fb1cd1a9 --- /dev/null +++ b/src/Http/Output/AbstractOutput.php @@ -0,0 +1,14 @@ +getContent()) && !is_null($response->getContent())) { + if (!$this->acceptsContent($response->getContent())) { throw new \Parable\Http\Exception('Can only work with string or null content'); } + return $response->getContent() ?: null; + } + + /** + * This output class only accepts string or null values. + * + * @inheritdoc + */ + public function acceptsContent($content) + { + return is_string($content) || $content === null; } } diff --git a/src/Http/Output/Json.php b/src/Http/Output/Json.php index fc6d83d6..83d6f584 100644 --- a/src/Http/Output/Json.php +++ b/src/Http/Output/Json.php @@ -2,7 +2,7 @@ namespace Parable\Http\Output; -class Json implements \Parable\Http\Output\OutputInterface +class Json extends \Parable\Http\Output\AbstractOutput { /** @var string */ protected $contentType = 'application/json'; @@ -33,8 +33,7 @@ public function prepare(\Parable\Http\Response $response) throw new \Parable\Http\Exception("Json encode error: '" . json_last_error_msg() . "'"); // @codeCoverageIgnore } - $response->setContent($content); - return $this; + return $content ?: null; } /** diff --git a/src/Http/Output/OutputInterface.php b/src/Http/Output/OutputInterface.php index 8dd649ef..86a7ef4c 100644 --- a/src/Http/Output/OutputInterface.php +++ b/src/Http/Output/OutputInterface.php @@ -14,11 +14,20 @@ interface OutputInterface public function init(\Parable\Http\Response $response); /** - * Prepare the content for output according to the output type. + * Prepare and return the content for output according to the output type. * * @param \Parable\Http\Response $response * - * @return $this + * @return string|null */ public function prepare(\Parable\Http\Response $response); + + /** + * Check whether the outputter accepts the type of content. + * + * @param mixed $content + * + * @return bool + */ + public function acceptsContent($content); } diff --git a/src/Http/Request.php b/src/Http/Request.php index 306c74e4..50b8bd39 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -4,12 +4,12 @@ class Request { - const METHOD_GET = "GET"; - const METHOD_POST = "POST"; - const METHOD_PUT = "PUT"; - const METHOD_PATCH = "PATCH"; - const METHOD_DELETE = "DELETE"; - const METHOD_OPTIONS = "OPTIONS"; + const METHOD_GET = 'GET'; + const METHOD_POST = 'POST'; + const METHOD_PUT = 'PUT'; + const METHOD_PATCH = 'PATCH'; + const METHOD_DELETE = 'DELETE'; + const METHOD_OPTIONS = 'OPTIONS'; const VALID_METHODS = [ self::METHOD_GET, @@ -37,7 +37,7 @@ class Request */ public function __construct() { - if (PHP_SAPI !== "cli") { + if (APP_CONTEXT === 'web') { $this->headers = getallheaders() ?: []; // @codeCoverageIgnore } } @@ -49,7 +49,7 @@ public function __construct() */ public function getCurrentUrl() { - return $this->getScheme() . "://" . $this->getHttpHost() . "/" . ltrim($this->getRequestUrl(), "/"); + return $this->getScheme() . '://' . $this->getHttpHost() . '/' . ltrim($this->getRequestUrl(), '/'); } /** @@ -59,7 +59,7 @@ public function getCurrentUrl() */ public function getProtocol() { - return isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : "HTTP/1.1"; + return isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1'; } /** @@ -79,7 +79,7 @@ public function getMethod() */ public function getRequestUrl() { - return isset($_SERVER["REQUEST_URI"]) ? $_SERVER["REQUEST_URI"] : null; + return isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null; } /** @@ -89,7 +89,7 @@ public function getRequestUrl() */ public function getScriptName() { - return isset($_SERVER["SCRIPT_NAME"]) ? $_SERVER["SCRIPT_NAME"] : null; + return isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : null; } /** @@ -187,7 +187,7 @@ public function isOptions() public function getHeader($key) { foreach ($this->headers as $header => $content) { - if (strtolower($key) == strtolower($header)) { + if (strtolower($key) === strtolower($header)) { return $content; } } @@ -213,26 +213,26 @@ public function getHeaders() */ public function getScheme() { - if (isset($_SERVER["REQUEST_SCHEME"])) { + if (isset($_SERVER['REQUEST_SCHEME'])) { // Apache 2.4+ - return $_SERVER["REQUEST_SCHEME"]; + return $_SERVER['REQUEST_SCHEME']; } - if (isset($_SERVER["REDIRECT_REQUEST_SCHEME"])) { - return $_SERVER["REDIRECT_REQUEST_SCHEME"]; + if (isset($_SERVER['REDIRECT_REQUEST_SCHEME'])) { + return $_SERVER['REDIRECT_REQUEST_SCHEME']; } - if (isset($_SERVER["HTTP_X_FORWARDED_PROTO"])) { + if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { // Sometimes available in proxy-forwarded requests - return $_SERVER["HTTP_X_FORWARDED_PROTO"]; + return $_SERVER['HTTP_X_FORWARDED_PROTO']; } - if (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] !== "off") { + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { // Old-style but compatible with IIS - return "https"; + return 'https'; } if (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) { // This doesn't say much, but this is our last attempt, so why not try - return "https"; + return 'https'; } - return "http"; + return 'http'; } /** @@ -242,19 +242,20 @@ public function getScheme() */ public function getHttpHost() { - if (isset($_SERVER["HTTP_HOST"]) && isset($_SERVER["SERVER_NAME"]) - && $_SERVER["HTTP_HOST"] === $_SERVER["SERVER_NAME"] + if (isset($_SERVER['HTTP_HOST']) + && isset($_SERVER['SERVER_NAME']) + && $_SERVER['HTTP_HOST'] === $_SERVER['SERVER_NAME'] ) { - return $_SERVER["HTTP_HOST"]; + return $_SERVER['HTTP_HOST']; } - if (isset($_SERVER["HTTP_HOST"])) { - return $_SERVER["HTTP_HOST"]; + if (isset($_SERVER['HTTP_HOST'])) { + return $_SERVER['HTTP_HOST']; } // This is the least reliable, due to the ability to spoof it - if (isset($_SERVER["SERVER_NAME"])) { - return $_SERVER["SERVER_NAME"]; + if (isset($_SERVER['SERVER_NAME'])) { + return $_SERVER['SERVER_NAME']; } return null; diff --git a/src/Http/Response.php b/src/Http/Response.php index e82e203f..e6cb951e 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -87,6 +87,12 @@ class Response /** @var bool */ protected $shouldTerminate = true; + /** @var string */ + protected $headerContent; + + /** @var string */ + protected $footerContent; + public function __construct( \Parable\Http\Request $request ) { @@ -171,29 +177,13 @@ public function setOutput(\Parable\Http\Output\OutputInterface $output) } /** - * Send the response. + * Return the output class. + * + * @return Output\OutputInterface */ - public function send() + public function getOutput() { - $buffered_content = $this->returnAllOutputBuffers(); - if (!empty($buffered_content) && is_string($this->content)) { - $this->content = $buffered_content . $this->content; - } - - $this->output->prepare($this); - - if (!headers_sent()) { - // @codeCoverageIgnoreStart - header("{$this->request->getProtocol()} {$this->getHttpCode()} {$this->getHttpCodeText()}"); - header("Content-type: {$this->getContentType()}"); - foreach ($this->getHeaders() as $key => $value) { - header("{$key}: {$value}"); - } - // @codeCoverageIgnoreEnd - } - - echo $this->getContent(); - $this->terminate(); + return $this->output; } /** @@ -268,6 +258,52 @@ public function clearContent() return $this; } + /** + * Set content to use as header. + * + * @param string $content + * + * @return $this + */ + public function setHeaderContent($content) + { + $this->headerContent = $content; + return $this; + } + + /** + * Return header content. + * + * @return string + */ + public function getHeaderContent() + { + return $this->headerContent ?: ""; + } + + /** + * Set content to use as header. + * + * @param string $content + * + * @return $this + */ + public function setFooterContent($content) + { + $this->footerContent = $content; + return $this; + } + + /** + * Return header content. + * + * @return string + */ + public function getFooterContent() + { + return $this->footerContent ?: ""; + } + /** * Start a new output buffer, upping the internal outputBufferLevel. * @@ -280,6 +316,28 @@ public function startOutputBuffer() return $this; } + /** + * Stop the current output buffer but do not return the output. + * + * @return $this + */ + public function stopOutputBuffer() + { + $this->returnOutputBuffer(); + return $this; + } + + /** + * Stop all output buffers but do not return the output. + * + * @return $this + */ + public function stopAllOutputBuffers() + { + $this->returnAllOutputBuffers(); + return $this; + } + /** * Return and end the current output buffer if output buffering was started with startOutputBuffer(). * @@ -288,7 +346,7 @@ public function startOutputBuffer() public function returnOutputBuffer() { if (!$this->isOutputBufferingEnabled()) { - return ""; + return ''; } $this->outputBufferLevel--; @@ -302,7 +360,7 @@ public function returnOutputBuffer() */ public function returnAllOutputBuffers() { - $content = ""; + $content = ''; if ($this->isOutputBufferingEnabled()) { while ($this->isOutputBufferingEnabled()) { @@ -407,8 +465,6 @@ public function clearHeaders() * Redirect to given url and stop processing. * * @param string $url - * - * @throws \Parable\Http\Exception */ public function redirect($url) { @@ -418,6 +474,40 @@ public function redirect($url) $this->terminate(); } + /** + * Build and send the response. + */ + public function send() + { + $buffered_content = $this->returnAllOutputBuffers(); + if (!empty($buffered_content) && is_string($this->content)) { + $this->content = $buffered_content . $this->content; + } + + $this->content = $this->output->prepare($this); + + if (!is_string($this->content) && $this->content !== null) { + $output = get_class($this->output); + throw new \Parable\Http\Exception("Output class '{$output}' did not result in string or null content."); + } + + if (!headers_sent()) { + // @codeCoverageIgnoreStart + header("{$this->request->getProtocol()} {$this->getHttpCode()} {$this->getHttpCodeText()}"); + header("Content-type: {$this->getContentType()}"); + foreach ($this->getHeaders() as $key => $value) { + header("{$key}: {$value}"); + } + // @codeCoverageIgnoreEnd + } + + echo $this->getHeaderContent(); + echo $this->getContent(); + echo $this->getFooterContent(); + + $this->terminate(); + } + /** * Set whether terminate should actually terminate or not. * diff --git a/src/Http/Url.php b/src/Http/Url.php index 75391cc5..4bbdee61 100644 --- a/src/Http/Url.php +++ b/src/Http/Url.php @@ -11,10 +11,10 @@ class Url protected $baseUrl; /** @var string */ - protected $basePath = "/public"; + protected $basePath = '/public'; /** @var string */ - protected $scriptName = "/index.php"; + protected $scriptName = '/index.php'; public function __construct( \Parable\Http\Request $request @@ -32,7 +32,7 @@ public function __construct( public function setBasePath($basePath) { if ($basePath) { - $basePath = "/" . trim($basePath, "/"); + $basePath = '/' . trim($basePath, '/'); } $this->basePath = $basePath; return $this; @@ -73,7 +73,7 @@ public function buildBaseUrl() if ($this->getBasePath()) { $basePathPos = strpos($url, $this->getBasePath()); if ($basePathPos !== false) { - $url = substr_replace($url, "", $basePathPos, strlen($this->getBasePath())); + $url = substr_replace($url, '', $basePathPos, strlen($this->getBasePath())); } } diff --git a/src/Log/Writer/File.php b/src/Log/Writer/File.php index 62905aeb..7809b3c5 100644 --- a/src/Log/Writer/File.php +++ b/src/Log/Writer/File.php @@ -34,11 +34,7 @@ public function write($message) */ public function setLogFile($logFile) { - if (!file_exists($logFile)) { - // At least attempt to create the file. - @touch($logFile); - } - if (!file_exists($logFile)) { + if (!$this->createfile($logFile)) { throw new \Parable\Log\Exception("Log file is not writable."); } @@ -46,6 +42,24 @@ public function setLogFile($logFile) return $this; } + /** + * Attempt to create the log file if it doesn't exist yet. + * + * @param string $logFile + * + * @return bool + * + * @codeCoverageIgnore + */ + protected function createFile($logFile) + { + if (!file_exists($logFile)) { + // At least attempt to create the file. + @touch($logFile); + } + return file_exists($logFile); + } + /** * Write the message to the log file. * diff --git a/src/Mail/Mailer.php b/src/Mail/Mailer.php index 64aa9400..30e78e37 100644 --- a/src/Mail/Mailer.php +++ b/src/Mail/Mailer.php @@ -86,8 +86,7 @@ public function setFrom($email, $name = null) */ public function addTo($email, $name = null) { - $this->addAddress('to', $email, $name); - return $this; + return $this->addAddress('to', $email, $name); } /** @@ -100,8 +99,7 @@ public function addTo($email, $name = null) */ public function addCc($email, $name = null) { - $this->addAddress('cc', $email, $name); - return $this; + return $this->addAddress('cc', $email, $name); } /** @@ -114,8 +112,7 @@ public function addCc($email, $name = null) */ public function addBcc($email, $name = null) { - $this->addAddress('bcc', $email, $name); - return $this; + return $this->addAddress('bcc', $email, $name); } /** @@ -125,6 +122,7 @@ public function addBcc($email, $name = null) * @param string $email * @param null|string $name * + * @return $this * @throws \Parable\Mail\Exception */ protected function addAddress($type, $email, $name = null) @@ -136,6 +134,7 @@ protected function addAddress($type, $email, $name = null) 'email' => $email, 'name' => $name, ]; + return $this; } /** diff --git a/src/ORM/Database.php b/src/ORM/Database.php index e4c42671..ba0d8d3f 100644 --- a/src/ORM/Database.php +++ b/src/ORM/Database.php @@ -386,16 +386,16 @@ public function query($query) public function setConfig(array $config) { foreach ($config as $type => $value) { - $property = ucwords(str_replace("-", " ", $type)); - $property = lcfirst(str_replace(" ", "", $property)); + $property = ucwords(str_replace('-', ' ', $type)); + $property = lcfirst(str_replace(' ', '', $property)); - $method = "set" . ucfirst($property); + $method = 'set' . ucfirst($property); if (method_exists($this, $method)) { - $this->$method($value); + $this->{$method}($value); } else { throw new \Parable\ORM\Exception( - "Tried to set non-existing config value '{$property}' on " . get_class($this) + "Tried to set non-existing property '{$property}' with value '{$value}' on " . get_class($this) ); } } diff --git a/src/ORM/Model.php b/src/ORM/Model.php index 2968c751..5f7cd8f8 100644 --- a/src/ORM/Model.php +++ b/src/ORM/Model.php @@ -207,6 +207,7 @@ public function getExportable() * @param bool $keepNullValue * * @return array + * @throws \ReflectionException */ public function toArray($keepNullValue = false) { @@ -277,6 +278,11 @@ public function toMappedArray(array $array) public function exportToArray() { $data = $this->toArray(); + + if (count($this->exportable) === 0) { + return $data; + } + $exportData = []; foreach ($data as $key => $value) { if (in_array($key, $this->exportable)) { @@ -316,6 +322,7 @@ public function removeEmptyValues(array $array) * Reset all public properties to null. * * @return $this + * @throws \ReflectionException */ public function reset() { diff --git a/src/ORM/Query.php b/src/ORM/Query.php index e18a3f32..9f0d297a 100644 --- a/src/ORM/Query.php +++ b/src/ORM/Query.php @@ -142,6 +142,7 @@ public function select(array $select) * Add a where condition set. * * @param \Parable\ORM\Query\ConditionSet $set + * * @return $this */ public function where(\Parable\ORM\Query\ConditionSet $set) @@ -154,12 +155,32 @@ public function where(\Parable\ORM\Query\ConditionSet $set) * Add an array of where condition sets. * * @param \Parable\ORM\Query\ConditionSet[] $sets + * + * @return $this */ public function whereMany(array $sets) { foreach ($sets as $set) { $this->where($set); } + return $this; + } + + /** + * Add a condition based on key/comparator/value, with an optional $tableName. + * + * @param string $key + * @param string $comparator + * @param string|null $value + * @param string|null $tableName + * + * @return $this + */ + public function whereCondition($key, $comparator, $value = null, $tableName = null) + { + return $this->where($this->buildAndSet([ + [$key, $comparator, $value, $tableName] + ])); } /** @@ -179,12 +200,15 @@ public function having(\Parable\ORM\Query\ConditionSet $set) * Add an array of having condition sets. * * @param \Parable\ORM\Query\ConditionSet[] $sets + * + * @return $this */ public function havingMany(array $sets) { foreach ($sets as $set) { $this->having($set); } + return $this; } /** @@ -214,31 +238,37 @@ public function buildOrSet(array $conditions) /** * Add a join to the query. * - * @param int $type - * @param string $tableName - * @param string $key - * @param string $comparator - * @param mixed $value - * @param bool $shouldCompareFields + * @param int $type + * @param string $joinTableName + * @param string $key + * @param string $comparator + * @param mixed $value + * @param bool $shouldCompareFields + * @param string|null $tableName * * @return $this */ protected function join( $type, - $tableName, + $joinTableName, $key, $comparator, $value = null, - $shouldCompareFields = true + $shouldCompareFields = true, + $tableName = null ) { + if (!$tableName) { + $tableName = $this->getTableName(); + } + $condition = new \Parable\ORM\Query\Condition(); $condition - ->setTableName($this->getTableName()) - ->setJoinTableName($tableName) + ->setQuery($this) + ->setTableName($tableName) + ->setJoinTableName($joinTableName) ->setKey($key) ->setComparator($comparator) ->setValue($value) - ->setQuery($this) ->setShouldCompareFields($shouldCompareFields); $this->joins[$type][] = $condition; @@ -248,65 +278,125 @@ protected function join( /** * Add an inner join to the query. * - * @param string $tableName - * @param string $key - * @param string $comparator - * @param mixed $value - * @param bool $shouldCompareFields + * @param string $joinTableName + * @param string $key + * @param string $comparator + * @param mixed $value + * @param bool $shouldCompareFields + * @param string|null $tableName * * @return $this */ - public function innerJoin($tableName, $key, $comparator, $value = null, $shouldCompareFields = true) - { - return $this->join(self::JOIN_INNER, $tableName, $key, $comparator, $value, $shouldCompareFields); + public function innerJoin( + $joinTableName, + $key, + $comparator, + $value = null, + $shouldCompareFields = true, + $tableName = null + ) { + return $this->join( + self::JOIN_INNER, + $joinTableName, + $key, + $comparator, + $value, + $shouldCompareFields, + $tableName + ); } /** * Add a left join to the query. * - * @param string $tableName - * @param string $key - * @param string $comparator - * @param mixed $value - * @param bool $shouldCompareFields + * @param string $joinTableName + * @param string $key + * @param string $comparator + * @param mixed $value + * @param bool $shouldCompareFields + * @param string|null $tableName * * @return $this */ - public function leftJoin($tableName, $key, $comparator, $value = null, $shouldCompareFields = true) - { - return $this->join(self::JOIN_LEFT, $tableName, $key, $comparator, $value, $shouldCompareFields); + public function leftJoin( + $joinTableName, + $key, + $comparator, + $value = null, + $shouldCompareFields = true, + $tableName = null + ) { + return $this->join( + self::JOIN_LEFT, + $joinTableName, + $key, + $comparator, + $value, + $shouldCompareFields, + $tableName + ); } /** * Add a right join to the query. * - * @param string $tableName - * @param string $key - * @param string $comparator - * @param mixed $value - * @param bool $shouldCompareFields + * @param string $joinTableName + * @param string $key + * @param string $comparator + * @param mixed $value + * @param bool $shouldCompareFields + * @param string|null $tableName * * @return $this */ - public function rightJoin($tableName, $key, $comparator, $value = null, $shouldCompareFields = true) - { - return $this->join(self::JOIN_RIGHT, $tableName, $key, $comparator, $value, $shouldCompareFields); + public function rightJoin( + $joinTableName, + $key, + $comparator, + $value = null, + $shouldCompareFields = true, + $tableName = null + ) { + return $this->join( + self::JOIN_RIGHT, + $joinTableName, + $key, + $comparator, + $value, + $shouldCompareFields, + $tableName + ); } /** * Add a full join to the query. * - * @param string $tableName - * @param string $key - * @param string $comparator - * @param mixed $value - * @param bool $shouldCompareFields + * @param string $joinTableName + * @param string $key + * @param string $comparator + * @param mixed $value + * @param bool $shouldCompareFields + * @param string|null $tableName * * @return $this */ - public function fullJoin($tableName, $key, $comparator, $value = null, $shouldCompareFields = true) - { - return $this->join(self::JOIN_FULL, $tableName, $key, $comparator, $value, $shouldCompareFields); + public function fullJoin( + $joinTableName, + $key, + $comparator, + $value = null, + $shouldCompareFields = true, + $tableName = null + ) { + return $this->join( + self::JOIN_FULL, + $joinTableName, + $key, + $comparator, + $value, + $shouldCompareFields, + $tableName + ); } /** @@ -436,7 +526,7 @@ protected function buildSelect() $selects[] = $select; } } - return implode(', ', $selects); + return 'SELECT ' . implode(', ', $selects); } /** @@ -450,17 +540,17 @@ protected function buildJoins() foreach ($this->joins as $type => $joins) { if (count($joins) > 0) { foreach ($joins as $join) { - if ($type == self::JOIN_INNER) { - $builtJoins[] = "INNER JOIN"; - } elseif ($type == self::JOIN_LEFT) { - $builtJoins[] = "LEFT JOIN"; - } elseif ($type == self::JOIN_RIGHT) { - $builtJoins[] = "RIGHT JOIN"; - } elseif ($type == self::JOIN_FULL) { - $builtJoins[] = "FULL JOIN"; + if ($type === self::JOIN_INNER) { + $builtJoins[] = 'INNER JOIN'; + } elseif ($type === self::JOIN_LEFT) { + $builtJoins[] = 'LEFT JOIN'; + } elseif ($type === self::JOIN_RIGHT) { + $builtJoins[] = 'RIGHT JOIN'; + } elseif ($type === self::JOIN_FULL) { + $builtJoins[] = 'FULL JOIN'; } - $builtJoins[] = $this->quoteIdentifier($join->getJoinTableName()) . " ON"; + $builtJoins[] = $this->quoteIdentifier($join->getJoinTableName()) . ' ON'; // Use a ConditionSet to build the joins $conditionSet = new Query\Condition\AndSet($this, [$join]); @@ -469,7 +559,7 @@ protected function buildJoins() } } - return implode(" ", $builtJoins); + return implode(' ', $builtJoins); } /** @@ -517,10 +607,10 @@ protected function buildOrderBy() $orders = []; foreach ($this->orderBy as $orderBy) { - $key = $this->quoteIdentifier($orderBy["tableName"]) . "." . $this->quoteIdentifier($orderBy["key"]); + $key = $this->quoteIdentifier($orderBy['tableName']) . '.' . $this->quoteIdentifier($orderBy['key']); $orders[] = $key . ' ' . $orderBy['direction']; } - return "ORDER BY " . implode(', ', $orders); + return 'ORDER BY ' . implode(', ', $orders); } /** @@ -536,10 +626,10 @@ protected function buildGroupBy() $groups = []; foreach ($this->groupBy as $groupBy) { - $groupBy = $this->quoteIdentifier($groupBy["tableName"]) . "." . $this->quoteIdentifier($groupBy["key"]); + $groupBy = $this->quoteIdentifier($groupBy['tableName']) . '.' . $this->quoteIdentifier($groupBy['key']); $groups[] = $groupBy; } - return "GROUP BY " . implode(', ', $groups); + return 'GROUP BY ' . implode(', ', $groups); } /** @@ -553,16 +643,16 @@ protected function buildLimitOffset() return ''; } - $limitOffset = ""; - if ($this->limitOffset["limit"] && $this->limitOffset["offset"]) { + $limitOffset = ''; + if ($this->limitOffset['limit'] && $this->limitOffset['offset']) { $limitOffset = $this->limitOffset['offset'] . ',' . $this->limitOffset['limit']; - } elseif ($this->limitOffset["limit"]) { - $limitOffset = $this->limitOffset["limit"]; - } elseif ($this->limitOffset["offset"]) { - $limitOffset = $this->limitOffset["offset"]; + } elseif ($this->limitOffset['limit']) { + $limitOffset = $this->limitOffset['limit']; + } elseif ($this->limitOffset['offset']) { + $limitOffset = $this->limitOffset['offset']; } - return "LIMIT " . $limitOffset; + return 'LIMIT ' . $limitOffset; } /** @@ -589,8 +679,8 @@ public function __toString() return ''; } - $query[] = "SELECT " . $this->buildSelect(); - $query[] = "FROM " . $this->getQuotedTableName(); + $query[] = $this->buildSelect(); + $query[] = 'FROM ' . $this->getQuotedTableName(); $query[] = $this->buildJoins(); $query[] = $this->buildWheres(); $query[] = $this->buildGroupBy(); @@ -602,14 +692,14 @@ public function __toString() return ''; } - $query[] = "DELETE FROM " . $this->getQuotedTableName(); + $query[] = 'DELETE FROM ' . $this->getQuotedTableName(); $query[] = $this->buildWheres(); } elseif ($this->action === 'update') { if (count($this->values) === 0 || count($this->where) === 0) { return ''; } - $query[] = "UPDATE " . $this->getQuotedTableName(); + $query[] = 'UPDATE ' . $this->getQuotedTableName(); $values = []; foreach ($this->values as $key => $value) { @@ -619,16 +709,16 @@ public function __toString() $correctValue = $this->quote($value); } $key = $this->quoteIdentifier($key); - $values[] = $key . " = " . $correctValue; + $values[] = $key . ' = ' . $correctValue; } - $query[] = "SET " . implode(', ', $values); + $query[] = 'SET ' . implode(', ', $values); $query[] = $this->buildWheres(); } elseif ($this->action === 'insert') { if (count($this->values) === 0) { return ''; } - $query[] = "INSERT INTO " . $this->getQuotedTableName(); + $query[] = 'INSERT INTO ' . $this->getQuotedTableName(); $keys = []; $values = []; @@ -643,9 +733,9 @@ public function __toString() $values[] = $correctValue; } - $query[] = "(" . implode(', ', $keys) . ")"; - $query[] = "VALUES"; - $query[] = "(" . implode(', ', $values) . ")"; + $query[] = '(' . implode(', ', $keys) . ')'; + $query[] = 'VALUES'; + $query[] = '(' . implode(', ', $values) . ')'; } @@ -657,7 +747,7 @@ public function __toString() } // Now make it nice. - $queryString = implode(" ", $query); + $queryString = implode(' ', $query); $queryString = trim($queryString) . ';'; // Since we got here, we've got a query to output diff --git a/src/ORM/Query/Condition.php b/src/ORM/Query/Condition.php index 0392e5df..6c1f65fd 100644 --- a/src/ORM/Query/Condition.php +++ b/src/ORM/Query/Condition.php @@ -276,7 +276,7 @@ public function build() // If we don't have IN/NOT IN, IS/NOT IS, and we shouldn't quote, we assume we're checking fields. if ($this->shouldCompareFields()) { $valueBuild = [ - $this->query->getQuotedTableName(), + $this->query->quoteIdentifier($this->getTableName()), '.', $this->query->quoteIdentifier($value), ]; diff --git a/src/ORM/Query/ConditionSet.php b/src/ORM/Query/ConditionSet.php index df53798d..9962b698 100644 --- a/src/ORM/Query/ConditionSet.php +++ b/src/ORM/Query/ConditionSet.php @@ -45,6 +45,9 @@ public function __construct( if (isset($condition[2])) { $conditionObject->setValue($condition[2]); } + if (isset($condition[3])) { + $conditionObject->setTableName($condition[3]); + } $this->conditions[] = $conditionObject; } } diff --git a/src/ORM/Repository.php b/src/ORM/Repository.php index 1a3a2ec0..bcd39731 100644 --- a/src/ORM/Repository.php +++ b/src/ORM/Repository.php @@ -107,7 +107,6 @@ public function getById($id) * @param mixed|null $value * * @return \Parable\ORM\Model[]|\Parable\ORM\Model - * @throws \Parable\ORM\Exception */ public function getByCondition($key, $comparator, $value = null) { @@ -227,7 +226,7 @@ public function createModel() } /** - * Set a model on the repository. Reset it so there's no unwanted values stored on it.. + * Set a model on the repository. Reset it so there's no unwanted values stored on it. * * @param \Parable\ORM\Model $model * @@ -240,7 +239,7 @@ public function setModel(\Parable\ORM\Model $model) } /** - * Return model. + * Return the model instance. * * @return null|\Parable\ORM\Model */ @@ -313,11 +312,12 @@ public function reset() * @param string $modelName * * @return \Parable\ORM\Repository + * @throws \Parable\ORM\Exception */ public static function createForModelName($modelName) { if (!class_exists($modelName)) { - throw new Exception("Model '{$modelName}' does not exist."); + throw new \Parable\ORM\Exception("Model '{$modelName}' does not exist."); } return self::createForModel($modelName::create()); } @@ -327,7 +327,7 @@ public static function createForModelName($modelName) * * @param \Parable\ORM\Model $model * - * @return Repository + * @return \Parable\ORM\Repository */ public static function createForModel(\Parable\ORM\Model $model) { diff --git a/src/Rights/Rights.php b/src/Rights/Rights.php index 0f7da346..e404c392 100644 --- a/src/Rights/Rights.php +++ b/src/Rights/Rights.php @@ -28,7 +28,7 @@ public function __construct() public function addRight($name) { $rights = $this->getRights(); - if (count($rights) == 0) { + if (count($rights) === 0) { $value = 1; } else { $value = 2 * end($rights); @@ -47,6 +47,16 @@ public function getRights() return $this->rights; } + /** + * Return all rights' names. + * + * @return string[] + */ + public function getRightsNames() + { + return array_keys($this->rights); + } + /** * Return a specific right by name. * @@ -103,9 +113,9 @@ public function combine(array $rights) */ public function getRightsFromNames(array $names) { - $rights_string = ""; + $rights_string = ''; foreach ($this->getRights() as $right => $value) { - $rights_string .= in_array($right, $names) ? "1" : "0"; + $rights_string .= in_array($right, $names) ? '1' : '0'; } return strrev($rights_string); } diff --git a/src/Routing/Route.php b/src/Routing/Route.php index a86d5673..0b316c3f 100644 --- a/src/Routing/Route.php +++ b/src/Routing/Route.php @@ -36,6 +36,7 @@ class Route * * @param array $data * + * @return $this * @throws \Parable\Routing\Exception */ public function setDataFromArray(array $data) @@ -43,22 +44,24 @@ public function setDataFromArray(array $data) foreach ($data as $property => $value) { $method = 'set' . ucfirst($property); if (method_exists($this, $method)) { - $this->$method($value); + $this->{$method}($value); } else { throw new \Parable\Routing\Exception( - "Tried to set non-existing property '{$property}' with value '{$value}' on Route." + "Tried to set non-existing property '{$property}' with value '{$value}' on " . get_class($this) ); } } $this->checkValidProperties(); $this->parseUrlParameters(); + + return $this; } /** * Set the methods accepted by this Route (POST, GET, PUT, etc.) and make sure they're uppercase. * - * @param stringp[ $methods + * @param string[] $methods * * @return $this */ @@ -90,8 +93,8 @@ public function getMethods() */ public function setUrl($url) { - if (strpos($url, "/") !== 0) { - $url = "/" . $url; + if (strpos($url, '/') !== 0) { + $url = '/' . $url; } $this->url = $url; return $this; @@ -225,6 +228,7 @@ public function getTemplatePath() /** * Check whether a valid set of properties is set. * + * @return $this * @throws \Parable\Routing\Exception */ public function checkValidProperties() @@ -235,6 +239,7 @@ public function checkValidProperties() if (empty($this->methods)) { throw new \Parable\Routing\Exception('Methods are required and must be passed as an array.'); } + return $this; } /** diff --git a/src/Routing/Router.php b/src/Routing/Router.php index 10092a4e..8c446beb 100644 --- a/src/Routing/Router.php +++ b/src/Routing/Router.php @@ -17,7 +17,6 @@ class Router */ public function addRoute($name, \Parable\Routing\Route $route) { - // Call checkValidProperties because invalid Routes have no place here. $route->checkValidProperties(); $this->routes[$name] = $route; diff --git a/src/defines.php b/src/defines.php index cc50da63..8e1c993b 100644 --- a/src/defines.php +++ b/src/defines.php @@ -1,8 +1,31 @@ addPackage($packageName); + } } diff --git a/tests/Base.php b/tests/Base.php index c122ed71..ae88d583 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -12,9 +12,13 @@ protected function setUp() parent::setUp(); // This key might be handy to have - $GLOBALS['_SESSION'] = []; + $GLOBALS["_SESSION"] = []; + $GLOBALS["_SERVER"]["argv"] = []; $this->testPath = \Parable\DI\Container::create(\Parable\Filesystem\Path::class); $this->testPath->setBaseDir(__DIR__ . DS . ".."); + + // Disable display errors + ini_set("display_errors", 0); } /** diff --git a/tests/Components/Console/AppTest.php b/tests/Components/Console/AppTest.php index b77b3d9b..c8e52b02 100644 --- a/tests/Components/Console/AppTest.php +++ b/tests/Components/Console/AppTest.php @@ -23,8 +23,6 @@ protected function setUp() { parent::setUp(); - $_SERVER["argv"] = []; - $this->parameter = new \Parable\Console\Parameter(); \Parable\DI\Container::store($this->parameter); @@ -94,6 +92,12 @@ public function testAppAddGetCommand() $this->assertSame('OK2', $commandGot->run()); } + public function testHasCommand() + { + $this->assertTrue($this->app->hasCommand('test1')); + $this->assertFalse($this->app->hasCommand('nope not this one')); + } + public function testAppGetCommandsReturnsAll() { $commands = $this->app->getCommands(); @@ -146,6 +150,21 @@ public function testPassCommandOnCommandLineRunsAppropriateCommand() $this->assertSame("OK1", $app->run()); } + public function testRemoveCommandbyName() + { + $app = new \Parable\Console\App(new \Parable\Console\Output(), new \Parable\Console\Input(), $this->parameter); + $app->addCommand($this->command1); + $app->addCommand($this->command2); + + $this->assertCount(2, $app->getCommands()); + + $app->removeCommandByName($this->command1->getName()); + + $this->assertCount(1, $app->getCommands()); + + $this->assertSame($this->command2, $app->getCommand($this->command2->getName())); + } + /** * @dataProvider dpTrueFalse * diff --git a/tests/Components/Console/OutputTest.php b/tests/Components/Console/OutputTest.php index 87015e4a..4dbd3c86 100644 --- a/tests/Components/Console/OutputTest.php +++ b/tests/Components/Console/OutputTest.php @@ -302,14 +302,14 @@ public function testParseTagsForRealThisTime() $this->assertSame($this->addTag('unknown'), $output->parseTags('unknown')); // Since tags are escaped with the defaultTag at the end, we'll need 2 - $this->assertSame($this->addTag("\e[0;32mgreen", 2), $output->parseTags('green')); - $this->assertSame($this->addTag("\e[0;31mred", 2), $output->parseTags('red')); + $this->assertSame($this->addTag("\e[;32mgreen", 2), $output->parseTags('green')); + $this->assertSame($this->addTag("\e[;31mred", 2), $output->parseTags('red')); // And a more complex one, with both a fore- and a background color // Since tags are escaped with the defaultTag at the end and there's two tags, we'll need 3 $this->assertSame( - $this->addTag("\e[0;31m\e[47mred on lightgray", 3), - $output->parseTags('red on lightgray') + $this->addTag("\e[;31m\e[47mred on lightgray", 3), + $output->parseTags('red on lightgray') ); } diff --git a/tests/Components/Framework/AppTest.php b/tests/Components/Framework/AppTest.php index 5a5233d5..ca3424c8 100644 --- a/tests/Components/Framework/AppTest.php +++ b/tests/Components/Framework/AppTest.php @@ -413,6 +413,36 @@ public function testSetDefaultTimezoneFromConfig() date_default_timezone_set($timezone); } + public function testLoadLayoutPicksUpCorrectLayoutContent() + { + $headerPath = $this->testPath->getDir('tests/TestTemplates/layout/header.phtml'); + $footerPath = $this->testPath->getDir('tests/TestTemplates/layout/footer.phtml'); + + $config = \Parable\DI\Container::create(\Parable\Tests\TestClasses\SettableConfig::class); + $config->set([ + "parable" => [ + "layout" => [ + "header" => $headerPath, + "footer" => $footerPath, + ], + ], + ]); + + $app = $this->createAppWithSpecificConfig($config); + $app->run(); + + $output = $this->getActualOutputAndClean(); + + $this->assertContains("HEADER FROM FILE", $output); + $this->assertContains("FOOTER FROM FILE", $output); + + $this->assertSame(0, strpos($output, "HEADER FROM FILE")); + + $footerPosition = strlen($output) - strlen("FOOTER FROM FILE"); + + $this->assertSame($footerPosition, strpos($output, "FOOTER FROM FILE")); + } + /** * @param string $mainConfigClassName * @return \Parable\Framework\App diff --git a/tests/Components/Framework/AuthenticationTest.php b/tests/Components/Framework/AuthenticationTest.php index d9d5dac2..54154296 100644 --- a/tests/Components/Framework/AuthenticationTest.php +++ b/tests/Components/Framework/AuthenticationTest.php @@ -48,6 +48,33 @@ public function testSetGetUserClassName() $this->authentication->setUser([]); } + public function testSetGetUserIdProperty() + { + $this->authenticateUser(); + + // Assert the default situation + $this->assertSame( + ["user_id" => $this->user->id], + $this->authentication->getAuthenticationData() + ); + + $this->authentication->setUserIdProperty("other_id"); + $this->authentication->reset(); + $this->authenticateUser(); + + // It'll be empty because we're looking for the wrong property at this point + $this->assertEmpty($this->authentication->getAuthenticationData()); + + $this->user->other_id = 1337; + $this->authenticateUser(); + + // The value now exists, so it should match + $this->assertSame( + ["user_id" => 1337], + $this->authentication->getAuthenticationData() + ); + } + public function testSetUserClassNameThrowsExceptionIfClassDoesntExist() { $this->expectException(\Parable\Framework\Exception::class); @@ -70,6 +97,11 @@ public function testSettingUserAndAuthenticatingWorksProperly() $this->assertSame($this->user, $this->authentication->getUser()); $this->assertTrue($this->authentication->initialize()); + + $this->assertSame( + ["user_id" => $this->user->id], + $this->authentication->getAuthenticationData() + ); } public function testInitializeExpectsCertainAuthenticationData() @@ -92,6 +124,36 @@ public function testInitializeExpectsExistingUser() $this->assertFalse($this->authentication->initialize()); } + public function testResetUserWorks() + { + $this->authenticateUser(); + + $this->assertNotNull($this->authentication->getUser()); + $this->assertNotEmpty($this->authentication->getAuthenticationData()); + + $this->authentication->resetUser(); + + $this->assertNull($this->authentication->getUser()); + $this->assertEmpty($this->authentication->getAuthenticationData()); + } + + public function testResetRevokesAuthAndUnsetsUser() + { + $this->authenticateUser(); + + $this->assertNotNull($this->authentication->getUser()); + $this->assertNotNull($this->session->get('auth')); + $this->assertTrue($this->authentication->initialize()); + $this->assertNotEmpty($this->authentication->getAuthenticationData()); + + $this->authentication->reset(); + + $this->assertNull($this->authentication->getUser()); + $this->assertNull($this->session->get('auth')); + $this->assertFalse($this->authentication->initialize()); + $this->assertEmpty($this->authentication->getAuthenticationData()); + } + /** * Sets an authenticatable user on the Authentication class and authenticates 'm. */ diff --git a/tests/Components/Framework/Base.php b/tests/Components/Framework/Base.php index 13619ad3..3a71db1a 100644 --- a/tests/Components/Framework/Base.php +++ b/tests/Components/Framework/Base.php @@ -13,15 +13,16 @@ protected function setUp() // Since many Framework components depend on \Parable\Http\Request, // which depends on some global values, we set them. - $GLOBALS['_SERVER']; + $GLOBALS["_SERVER"]; $GLOBALS['_SERVER'] = [ - 'REQUEST_METHOD' => "GET", - 'REQUEST_SCHEME' => 'http', - 'HTTP_HOST' => 'www.test.dev', - 'SCRIPT_NAME' => '/test/public/index.php', + "argv" => [], + "REQUEST_METHOD" => "GET", + "REQUEST_SCHEME" => "http", + "HTTP_HOST" => "www.test.dev", + "SCRIPT_NAME" => "/test/public/index.php", ]; - $GLOBALS['_SESSION'] = []; + $GLOBALS["_SESSION"] = []; } } diff --git a/tests/Components/Framework/ConfigTest.php b/tests/Components/Framework/ConfigTest.php index 4f3c1364..ae8c2b92 100644 --- a/tests/Components/Framework/ConfigTest.php +++ b/tests/Components/Framework/ConfigTest.php @@ -46,7 +46,7 @@ public function testSetMainConfigClassThrowsExceptionIfNotExist() public function testGetConfig() { $this->config->addConfig($this->config1); - $this->assertSame("primary value", $this->config->get('setting')); + $this->assertSame('primary value', $this->config->get('setting')); } public function testGetConfigNonExistingValueReturnsNull() @@ -80,9 +80,9 @@ public function testGetNestedConfigValues() public function testGetConfigSetsDataBasedOnAddConfigTiming() { $this->config->addConfig($this->config1); - $this->assertSame("primary value", $this->config->get('setting')); + $this->assertSame('primary value', $this->config->get('setting')); $this->config->addConfig($this->config2); - $this->assertSame("secondary value", $this->config->get('setting')); + $this->assertSame('secondary value', $this->config->get('setting')); } } diff --git a/tests/Components/Framework/ConsoleAppTest.php b/tests/Components/Framework/ConsoleAppTest.php new file mode 100644 index 00000000..debd8304 --- /dev/null +++ b/tests/Components/Framework/ConsoleAppTest.php @@ -0,0 +1,36 @@ +consoleApp = \Parable\DI\Container::createAll(\Parable\Framework\ConsoleApp::class); + } + + public function testErrorReporting() + { + $this->consoleApp->setErrorReportingEnabled(true); + $this->assertSame("1", ini_get("display_errors")); + $this->assertTrue($this->consoleApp->isErrorReportingEnabled()); + + $this->consoleApp->setErrorReportingEnabled(false); + $this->assertSame("0", ini_get("display_errors")); + $this->assertFalse($this->consoleApp->isErrorReportingEnabled()); + } + + public function testConsoleApp() + { + $this->consoleApp->run(); + + $output = $this->getActualOutputAndClean(); + + $this->assertContains("Parable " . \Parable\Framework\App::PARABLE_VERSION, $output); + } +} diff --git a/tests/Components/Framework/DispatcherTest.php b/tests/Components/Framework/DispatcherTest.php index 00abf71a..126b2110 100644 --- a/tests/Components/Framework/DispatcherTest.php +++ b/tests/Components/Framework/DispatcherTest.php @@ -98,4 +98,27 @@ public function testGetRouteReturnsRouteIfRouteDispatched() $this->assertSame($route, $this->dispatcher->getDispatchedRoute()); } + + public function testExceptionIsThrownIfCallableReturnsUnacceptableContent() + { + $this->expectException(\Parable\Framework\Exception::class); + $this->expectExceptionMessage( + "Route returned value of type 'array', which output class 'Parable\Http\Output\Html' cannot handle." + ); + + $this->response->setOutput(new \Parable\Http\Output\Html()); + + $route = new \Parable\Routing\Route(); + $route->setDataFromArray([ + 'methods' => ['GET'], + 'url' => '/', + 'callable' => function () { + return ["array data" => "bad"]; + } + ]); + + $this->dispatcher->dispatch($route); + + $this->assertSame($route, $this->dispatcher->getDispatchedRoute()); + } } diff --git a/tests/Components/Framework/Loader/CommandLoaderTest.php b/tests/Components/Framework/Loader/CommandLoaderTest.php new file mode 100644 index 00000000..2c1e84c4 --- /dev/null +++ b/tests/Components/Framework/Loader/CommandLoaderTest.php @@ -0,0 +1,41 @@ +app = \Parable\DI\Container::get(\Parable\Console\App::class); + $this->commandLoader = \Parable\DI\Container::get(\Parable\Framework\Loader\CommandLoader::class); + } + + public function testLoad() + { + $this->assertCount(0, $this->app->getCommands()); + + $command = \Parable\DI\Container::create(\Parable\Tests\TestClasses\Command::class); + + $this->commandLoader->load([ + get_class($command) + ]); + + $this->assertCount(1, $this->app->getCommands()); + $this->assertInstanceOf( + \Parable\Tests\TestClasses\Command::class, + $this->app->getCommand($command->getName()) + ); + $this->assertInstanceOf( + \Parable\Console\Command::class, + $this->app->getCommand($command->getName()) + ); + } +} diff --git a/tests/Components/Framework/Loader/InitLoaderTest.php b/tests/Components/Framework/Loader/InitLoaderTest.php new file mode 100644 index 00000000..b20af871 --- /dev/null +++ b/tests/Components/Framework/Loader/InitLoaderTest.php @@ -0,0 +1,25 @@ +initLoader = \Parable\DI\Container::create(\Parable\Framework\Loader\InitLoader::class); + } + + public function testLoad() + { + $this->initLoader->load([ + \Parable\Tests\TestClasses\Init\TestEcho::class + ]); + + $this->assertSame("This init was loaded.", $this->getActualOutputAndClean()); + } +} diff --git a/tests/Components/Framework/PackageManagerTest.php b/tests/Components/Framework/PackageManagerTest.php new file mode 100644 index 00000000..ce05bbe5 --- /dev/null +++ b/tests/Components/Framework/PackageManagerTest.php @@ -0,0 +1,65 @@ +app = \Parable\DI\Container::get(\Parable\Console\App::class); + $this->packageManager = \Parable\DI\Container::get(\Parable\Framework\Package\PackageManager::class); + } + + public function testAddPackage() + { + $this->packageManager->addPackage(\Parable\Tests\TestClasses\PackageTest::class); + + $this->assertSame( + [\Parable\Tests\TestClasses\PackageTest::class], + $this->liberateProperty($this->packageManager, "packages") + ); + } + + public function testRegister() + { + // There should be no commands + $this->assertCount(0, $this->app->getCommands()); + + $this->packageManager->addPackage(\Parable\Tests\TestClasses\PackageTest::class); + $this->packageManager->registerPackages(); + + // There should now be one command + $this->assertCount(1, $this->app->getCommands()); + $this->assertInstanceOf( + \Parable\Tests\TestClasses\Command::class, + $this->app->getCommand("testcommand") + ); + $this->assertInstanceOf( + \Parable\Console\Command::class, + $this->app->getCommand("testcommand") + ); + + // And the init echoes something + $this->assertSame("This init was loaded.", $this->getActualOutputAndClean()); + } + + public function testRegisterEmptyPackageDoesNothing() + { + // There should be no commands + $this->assertCount(0, $this->app->getCommands()); + + $this->packageManager->addPackage(\Parable\Tests\TestClasses\PackageTestEmpty::class); + $this->packageManager->registerPackages(); + + // There should now still have no commands + $this->assertCount(0, $this->app->getCommands()); + } +} diff --git a/tests/Components/GetSet/GetSetTest.php b/tests/Components/GetSet/GetSetTest.php index 1ee84ddc..8534a565 100644 --- a/tests/Components/GetSet/GetSetTest.php +++ b/tests/Components/GetSet/GetSetTest.php @@ -18,7 +18,7 @@ public function testSetAndGetResource() { $this->assertSame('test', $this->getSet->getResource()); - $this->getSet->setResource("what"); + $this->getSet->setResource('what'); $this->assertSame('what', $this->getSet->getResource()); } @@ -90,7 +90,7 @@ public function testRemoveNonExistingKeyDoesNothing() $this->assertInstanceOf( \Parable\GetSet\Base::class, - $this->getSet->remove("stuff") + $this->getSet->remove('stuff') ); $this->assertSame(3, $this->getSet->count()); @@ -171,30 +171,40 @@ public function testGetAllReturnsEmptyArrayIfNoResourceSet() $this->assertSame([], $getset->getAll()); } + public function testGetAllThrowsExcpetionIfNoLocalResourceSetAndResourceIsNotValidGlobal() + { + $this->expectException(\Parable\GetSet\Exception::class); + $this->expectExceptionMessage("Attempting to use global resource 'test' but resource not available."); + + $getset = new \Parable\Tests\TestClasses\TestGetSetNoResource(); + $getset->setResource('test'); + $getset->getAll(); + } + public function testGetSetAndRemoveWithHierarchalKeys() { - $this->getSet->set("one", ["this" => "should stay"]); - $this->getSet->set("one.two.three.four", "totally nested, yo"); + $this->getSet->set('one', ['this' => 'should stay']); + $this->getSet->set('one.two.three.four', 'totally nested, yo'); $this->assertSame( [ - "this" => "should stay", - "two" => [ - "three" => [ - "four" => "totally nested, yo", + 'this' => 'should stay', + 'two' => [ + 'three' => [ + 'four' => 'totally nested, yo', ], ], ], - $this->getSet->get("one") + $this->getSet->get('one') ); $this->assertSame( [ - "one" => [ - "this" => "should stay", - "two" => [ - "three" => [ - "four" => "totally nested, yo", + 'one' => [ + 'this' => 'should stay', + 'two' => [ + 'three' => [ + 'four' => 'totally nested, yo', ], ], ], @@ -202,14 +212,14 @@ public function testGetSetAndRemoveWithHierarchalKeys() $this->getSet->getAll() ); - $this->getSet->remove("one.this"); + $this->getSet->remove('one.this'); $this->assertSame( [ - "one" => [ - "two" => [ - "three" => [ - "four" => "totally nested, yo", + 'one' => [ + 'two' => [ + 'three' => [ + 'four' => 'totally nested, yo', ], ], ], @@ -219,16 +229,16 @@ public function testGetSetAndRemoveWithHierarchalKeys() $this->assertSame( [ - "four" => "totally nested, yo", + 'four' => 'totally nested, yo', ], - $this->getSet->getAndRemove("one.two.three") + $this->getSet->getAndRemove('one.two.three') ); - // And since "three" is now removed, "two" will be empty. + // And since 'three' is now removed, 'two' will be empty. $this->assertSame( [ - "one" => [ - "two" => [ + 'one' => [ + 'two' => [ ], ], ], @@ -238,34 +248,34 @@ public function testGetSetAndRemoveWithHierarchalKeys() public function testRemoveHierarchalKey() { - $this->getSet->set("one.two.three", "totally"); - $this->getSet->set("one.two.four", "also"); + $this->getSet->set('one.two.three', 'totally'); + $this->getSet->set('one.two.four', 'also'); - $this->assertCount(2, $this->getSet->get("one.two")); + $this->assertCount(2, $this->getSet->get('one.two')); - $this->getSet->remove("one.two.three"); + $this->getSet->remove('one.two.three'); - $this->assertCount(1, $this->getSet->get("one.two")); - $this->assertTrue(is_array($this->getSet->get("one.two"))); + $this->assertCount(1, $this->getSet->get('one.two')); + $this->assertTrue(is_array($this->getSet->get('one.two'))); - $this->getSet->remove("one.two"); + $this->getSet->remove('one.two'); - $this->assertNull($this->getSet->get("one.two")); + $this->assertNull($this->getSet->get('one.two')); // But one should be untouched and still an array - $this->assertTrue(is_array($this->getSet->get("one"))); + $this->assertTrue(is_array($this->getSet->get('one'))); } public function testGetNonExistingKeyReturnsDefault() { // Test non-existing should still be null - $this->assertNull($this->getSet->get("nope")); + $this->assertNull($this->getSet->get('nope')); // But with default it should be default - $this->assertEquals("default", $this->getSet->get("nope", "default")); + $this->assertEquals('default', $this->getSet->get('nope', 'default')); // Same for nested - $this->getSet->set("this", ["totally" => "exists"]); + $this->getSet->set('this', ['totally' => 'exists']); $this->assertNull($this->getSet->get("this.doesn't")); diff --git a/tests/Components/GetSet/InputStreamTest.php b/tests/Components/GetSet/InputStreamTest.php index 32332f1c..d841effc 100644 --- a/tests/Components/GetSet/InputStreamTest.php +++ b/tests/Components/GetSet/InputStreamTest.php @@ -7,13 +7,13 @@ class InputStreamTest extends \Parable\Tests\Base public function testRawDataParsedProperly() { $getSet = $this->setRawSource(); - $this->assertSame("value-from-raw", $getSet->get("test")); + $this->assertSame('value-from-raw', $getSet->get('test')); } public function testJsonDataParsedProperly() { $getSet = $this->setJsonSource(); - $this->assertSame("value-from-json", $getSet->get("test")); + $this->assertSame('value-from-json', $getSet->get('test')); } /** @@ -21,7 +21,7 @@ public function testJsonDataParsedProperly() */ protected function setRawSource() { - return $this->setSource(__DIR__ . "/Files/InputSourceRaw.txt"); + return $this->setSource(__DIR__ . '/Files/InputSourceRaw.txt'); } /** @@ -29,7 +29,7 @@ protected function setRawSource() */ protected function setJsonSource() { - return $this->setSource(__DIR__ . "/Files/InputSourceJson.txt"); + return $this->setSource(__DIR__ . '/Files/InputSourceJson.txt'); } /** @@ -40,7 +40,7 @@ protected function setJsonSource() protected function setSource($path) { $getSet = new \Parable\GetSet\InputStream(); - $this->mockProperty($getSet, "inputSource", $path); + $this->mockProperty($getSet, 'inputSource', $path); $getSet->__construct(); return $getSet; diff --git a/tests/Components/Http/Output/HtmlTest.php b/tests/Components/Http/Output/HtmlTest.php index 00477227..dc36f3b0 100644 --- a/tests/Components/Http/Output/HtmlTest.php +++ b/tests/Components/Http/Output/HtmlTest.php @@ -47,9 +47,9 @@ public function testPrepare() $response->setContent("this is content"); - $this->html->prepare($response); + $content = $this->html->prepare($response); - $this->assertSame("this is content", $response->getContent()); + $this->assertSame("this is content", $content); } /** @@ -78,4 +78,24 @@ public function dpInvalidContentTypes() [true], ]; } + + /** + * @dataProvider dpDataTypes + */ + public function testAcceptsContentSaysYesStringAndNullOnly($type, $expectedBoolValue) + { + $this->assertSame($expectedBoolValue, $this->html->acceptsContent($type)); + } + + public function dpDataTypes() + { + return [ + [null, true], + ["string", true], + [1337, false], + [true, false], + [new \stdClass(), false], + [["array"], false], + ]; + } } diff --git a/tests/Components/Http/Output/JsonTest.php b/tests/Components/Http/Output/JsonTest.php index 5d9919ea..bed17db6 100644 --- a/tests/Components/Http/Output/JsonTest.php +++ b/tests/Components/Http/Output/JsonTest.php @@ -35,9 +35,9 @@ public function testPrepare() 'secondary' => 'what now', ]); - $this->json->prepare($response); + $content = $this->json->prepare($response); - $this->assertSame('{"value":"stuff","secondary":"what now"}', $response->getContent()); + $this->assertSame('{"value":"stuff","secondary":"what now"}', $content); } public function testInvalidJsonStringStaysString() @@ -47,8 +47,28 @@ public function testInvalidJsonStringStaysString() $response->setContent("{[{[}]}]"); - $this->json->prepare($response); + $content = $this->json->prepare($response); - $this->assertSame('"{[{[}]}]"', $response->getContent()); + $this->assertSame('"{[{[}]}]"', $content); + } + + /** + * @dataProvider dpDataTypes + */ + public function testAcceptsContentSaysYesToEverything($type) + { + $this->assertTrue($this->json->acceptsContent($type)); + } + + public function dpDataTypes() + { + return [ + [null], + ["string"], + [1337], + [true], + [new \stdClass()], + [["array"]], + ]; } } diff --git a/tests/Components/Http/RequestTest.php b/tests/Components/Http/RequestTest.php index 4974c8fb..45f057b0 100644 --- a/tests/Components/Http/RequestTest.php +++ b/tests/Components/Http/RequestTest.php @@ -12,10 +12,10 @@ protected function setUp() parent::setUp(); // We need to set up the $_SERVER array first - $_SERVER['REQUEST_METHOD'] = "GET"; - $_SERVER['HTTP_HOST'] = "test.dev"; - $_SERVER['REQUEST_URI'] = "/folder/being/requested"; - $_SERVER['SCRIPT_NAME'] = "stuff"; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['HTTP_HOST'] = 'test.dev'; + $_SERVER['REQUEST_URI'] = '/folder/being/requested'; + $_SERVER['SCRIPT_NAME'] = 'stuff'; $this->request = new \Parable\Http\Request(); @@ -24,14 +24,14 @@ protected function setUp() public function testGetProtocol() { - // Default is HTTP/1.1, if, for example, SERVER_PROTOCOL is unset. - $this->assertSame("HTTP/1.1", $this->request->getProtocol()); + // Default is HTTP/1.1, if, for example, SERVER_PROTOCOL is unset + $this->assertSame('HTTP/1.1', $this->request->getProtocol()); - $_SERVER['SERVER_PROTOCOL'] = "HTTP/2.0"; + $_SERVER['SERVER_PROTOCOL'] = 'HTTP/2.0'; - $this->assertSame("HTTP/2.0", $this->request->getProtocol()); + $this->assertSame('HTTP/2.0', $this->request->getProtocol()); - // In CLI mode, this value shouldn't be here.x + // In CLI mode, this value shouldn't be here unset($_SERVER['SERVER_PROTOCOL']); } @@ -160,7 +160,7 @@ public function testGetSchemeWithAllPossibilities($scheme) $_SERVER = ['HTTPS' => ($scheme == 'http' ? 'off' : 'on')]; $this->assertSame($scheme, $this->request->getScheme()); - $_SERVER = ['SERVER_PORT' => ($scheme == 'http' ? 80 : 443)]; + $_SERVER = ['SERVER_PORT' => ($scheme == 'http' ? '80' : '443')]; $this->assertSame($scheme, $this->request->getScheme()); unset($_SERVER['SERVER_PORT']); diff --git a/tests/Components/Http/ResponseTest.php b/tests/Components/Http/ResponseTest.php index 03f25f86..47a1ae44 100644 --- a/tests/Components/Http/ResponseTest.php +++ b/tests/Components/Http/ResponseTest.php @@ -89,6 +89,22 @@ public function testClearContent() $this->assertSame("New!", $this->response->getContent()); } + public function testHeaderAndFooterContent() + { + $this->assertEmpty($this->response->getHeaderContent()); + + $this->response->setHeaderContent(""); + $this->response->setFooterContent(""); + + $this->response->setContent("Stuff goes here."); + + $this->assertSame("Stuff goes here.", $this->response->getContent()); + + $this->response->send(); + + $this->assertSame("Stuff goes here.", $this->getActualOutputAndClean()); + } + public function testAppendAndPrependContent() { $this->response->setContent('yo2'); @@ -163,6 +179,38 @@ public function testOutputBuffering() $this->assertSame(null, $this->response->getContent()); } + public function testStopOutputBuffering() + { + $this->response->startOutputBuffer(); + + $this->assertTrue($this->response->isOutputBufferingEnabled()); + + $this->response->stopOutputBuffer(); + + $this->assertFalse($this->response->isOutputBufferingEnabled()); + } + + public function testStopAllOutputBuffering() + { + $this->response->startOutputBuffer(); + $this->response->startOutputBuffer(); + $this->response->startOutputBuffer(); + $this->response->startOutputBuffer(); + $this->response->startOutputBuffer(); + + $this->assertTrue($this->response->isOutputBufferingEnabled()); + + $this->response->stopOutputBuffer(); + + // One output buffer stop isn't enough + $this->assertTrue($this->response->isOutputBufferingEnabled()); + + // But this one is. + $this->response->stopAllOutputBuffers(); + + $this->assertFalse($this->response->isOutputBufferingEnabled()); + } + public function testReturnOutputBufferReturnsEmptyStringIfNotStarted() { $this->assertSame("", $this->response->returnOutputBuffer()); @@ -265,4 +313,15 @@ public function testRedirect() $this->responseMock->expects($this->once())->method('terminate'); $this->responseMock->redirect('http://www.test.dev/redirected'); } + + public function testOutputPrepareReturningNonStringValueThrowsException() + { + $this->expectException(\Parable\Http\Exception::class); + $this->expectExceptionMessage( + "Output class 'Parable\Tests\TestClasses\Http\FaultyOutput' did not result in string or null content." + ); + + $this->response->setOutput(new \Parable\Tests\TestClasses\Http\FaultyOutput()); + $this->response->send(); + } } diff --git a/tests/Components/Log/LoggerTest.php b/tests/Components/Log/LoggerTest.php index 8b4f98ba..93083b9e 100644 --- a/tests/Components/Log/LoggerTest.php +++ b/tests/Components/Log/LoggerTest.php @@ -24,7 +24,7 @@ protected function setUp() $this->logger = new \Parable\Log\Logger(); - $this->fileWriter = $this->createPartialMock(\Parable\Log\Writer\File::class, ['writeToFile']); + $this->fileWriter = $this->createPartialMock(\Parable\Log\Writer\File::class, ['writeToFile', 'createFile']); } public function testLoggerWithoutWriterThrowsExceptionCallingWrite() @@ -71,11 +71,13 @@ public function testFileWriter() { $this->fileWriter ->method('writeToFile') - ->withAnyParameters() ->willReturnCallback(function ($message) { // To prevent having to actually write to a file, we just add everything to a property $this->recentLogLines .= $message . PHP_EOL; }); + $this->fileWriter + ->method('createFile') + ->willReturn(true); $this->fileWriter->setLogFile($this->logFile); $this->logger->setWriter($this->fileWriter); @@ -95,12 +97,15 @@ public function testFileWriterDealsWithObjectsArraysBoolsProperly() { $this->fileWriter ->method('writeToFile') - ->withAnyParameters() ->willReturnCallback(function ($message) { // To prevent having to actually write to a file, we just add everything to a property $this->recentLogLines .= $message . PHP_EOL; }); + $this->fileWriter + ->method('createFile') + ->willReturn(true); + $this->fileWriter->setLogFile($this->logFile); $this->logger->setWriter($this->fileWriter); @@ -123,6 +128,10 @@ public function testFileWriterLoggerThrowsExceptionCallingWriteWithoutLogFileSet public function testFileWriterLoggerThrowsExceptionIfLogfileUnwritable() { + $this->fileWriter + ->method('createFile') + ->willReturn(false); + $this->expectException(\Parable\Log\Exception::class); $this->expectExceptionMessage("Log file is not writable."); diff --git a/tests/Components/ORM/DatabaseTest.php b/tests/Components/ORM/DatabaseTest.php index 2a7d14d7..86913a7e 100644 --- a/tests/Components/ORM/DatabaseTest.php +++ b/tests/Components/ORM/DatabaseTest.php @@ -118,7 +118,9 @@ public function testSetConfig() public function testSetConfigThrowsExceptionOnNonExistingKeys() { $this->expectException(\Parable\ORM\Exception::class); - $this->expectExceptionMessage("Tried to set non-existing config value 'stuff' on Parable\ORM\Database"); + $this->expectExceptionMessage( + "Tried to set non-existing property 'stuff' with value 'yay' on Parable\ORM\Database" + ); $this->database->setConfig(['stuff' => 'yay']); } diff --git a/tests/Components/ORM/ModelTest.php b/tests/Components/ORM/ModelTest.php index 22deec32..d97c8adc 100644 --- a/tests/Components/ORM/ModelTest.php +++ b/tests/Components/ORM/ModelTest.php @@ -2,23 +2,21 @@ namespace Parable\Tests\Components\ORM; -use \Parable\Tests\TestClasses\Model; - class ModelTest extends \Parable\Tests\Components\ORM\Base { - /** @var Model */ + /** @var \Parable\Tests\TestClasses\Model */ protected $model; protected function setUp() { parent::setUp(); - $this->model = new Model($this->database); + $this->model = new \Parable\Tests\TestClasses\Model($this->database); } public function testCreate() { - $model = Model::create(); + $model = \Parable\Tests\TestClasses\Model::create(); // Two different instances are not the same. $this->assertNotSame($model, $this->model); @@ -173,6 +171,36 @@ public function testModelExportToArrayWithoutEmptyValues() $this->assertArrayNotHasKey('password', $modelArray); } + public function testExportToArrayWithoutExportablePropertyExportsAll() + { + $this->model->username = 'testuser'; + $this->model->password = 'password'; + $this->model->created_at = null; + + $this->assertSame( + [ + 'username' => 'testuser', + 'email' => null, + ], + $this->model->exportToArray() + ); + + // Now empty exportable + $this->mockProperty($this->model, "exportable", []); + + $this->assertSame( + [ + 'username' => 'testuser', + 'password' => 'password', + 'email' => null, + 'created_at' => null, + 'updated_at' => null, + 'id' => null, + ], + $this->model->exportToArray() + ); + } + public function testSetTableKey() { $this->model->setTableKey('key'); diff --git a/tests/Components/ORM/Query/Condition/AndOrSetTest.php b/tests/Components/ORM/Query/Condition/AndOrSetTest.php index 68592bd9..08302915 100644 --- a/tests/Components/ORM/Query/Condition/AndOrSetTest.php +++ b/tests/Components/ORM/Query/Condition/AndOrSetTest.php @@ -30,6 +30,21 @@ public function testCreateNewAndConditionSetWithArrayConditions() $this->assertSame("`user`.`id` = '1' AND `user`.`active` IN ('0','1')", $andSet->buildWithoutParentheses()); } + public function testCreateNewAndConditionSetWithArrayConditionsAndTableName() + { + $conditions = [ + ['id', '=', 1], + ['active', 'in', [0, 1]], + ['user_id', '=', 1, 'othertable'] + ]; + $andSet = new \Parable\ORM\Query\Condition\AndSet($this->query, $conditions); + + $this->assertSame( + "(`user`.`id` = '1' AND `user`.`active` IN ('0','1') AND `othertable`.`user_id` = '1')", + $andSet->buildWithParentheses() + ); + } + public function testCreateNewAndConditionSetWithConditionObjects() { $conditions = [ diff --git a/tests/Components/ORM/QueryTest.php b/tests/Components/ORM/QueryTest.php index a1cca4df..1b6233ca 100644 --- a/tests/Components/ORM/QueryTest.php +++ b/tests/Components/ORM/QueryTest.php @@ -111,6 +111,17 @@ public function testSelectAndWhereMany() ); } + public function testWhereCondition() + { + $this->query->whereCondition("id", "=", 1); + $this->query->whereCondition("updated_at", "is null"); + + $this->assertSame( + "SELECT * FROM `user` WHERE (`user`.`id` = '1') AND (`user`.`updated_at` IS NULL);", + (string)$this->query + ); + } + public function testSelectWhereValueTypes() { $this->query->where($this->query->buildAndSet([ diff --git a/tests/Components/Rights/RightsTest.php b/tests/Components/Rights/RightsTest.php index 07e70ad9..d1b143a9 100644 --- a/tests/Components/Rights/RightsTest.php +++ b/tests/Components/Rights/RightsTest.php @@ -99,4 +99,17 @@ public function testGetRightsFromNamesWithCustomRightsWorksToo() $this->rights->getRightsFromNames(["create", "update", "hello", "destroy_humanity"]) ); } + + public function testGetRightsNames() + { + $this->assertSame( + [ + 'create', + 'read', + 'update', + 'delete', + ], + $this->rights->getRightsNames() + ); + } } diff --git a/tests/Components/Routing/RouteTest.php b/tests/Components/Routing/RouteTest.php index 733fa0f8..e37862ce 100644 --- a/tests/Components/Routing/RouteTest.php +++ b/tests/Components/Routing/RouteTest.php @@ -51,7 +51,9 @@ public function testSetDataFromArray() public function testSetDataFromArrayThrowsExceptionOnInvalidProperty() { $this->expectException(\Parable\Routing\Exception::class); - $this->expectExceptionMessage("Tried to set non-existing property 'naww' with value 'index' on Route."); + $this->expectExceptionMessage( + "Tried to set non-existing property 'naww' with value 'index' on Parable\Routing\Route" + ); $route = new \Parable\Routing\Route(); $route->setDataFromArray([ "methods" => ["get"], diff --git a/tests/TestClasses/Http/FaultyOutput.php b/tests/TestClasses/Http/FaultyOutput.php new file mode 100644 index 00000000..c3816981 --- /dev/null +++ b/tests/TestClasses/Http/FaultyOutput.php @@ -0,0 +1,27 @@ +setContentType($this->contentType); + return $this; + } + + /** + * @inheritdoc + */ + public function prepare(\Parable\Http\Response $response) + { + // We always set the content to be an array, even IF + return ["this is an array and that's invalid"]; + } +} diff --git a/tests/TestClasses/Init/Test.php b/tests/TestClasses/Init/Test.php index 62a4f92d..532fc0fa 100644 --- a/tests/TestClasses/Init/Test.php +++ b/tests/TestClasses/Init/Test.php @@ -4,4 +4,7 @@ class Test { + public function __construct() + { + } } diff --git a/tests/TestClasses/Init/TestEcho.php b/tests/TestClasses/Init/TestEcho.php new file mode 100644 index 00000000..9189a50e --- /dev/null +++ b/tests/TestClasses/Init/TestEcho.php @@ -0,0 +1,11 @@ +