├── .gitignore ├── README.md ├── src └── Wave │ ├── Storage.php │ ├── Router │ ├── RoutingException.php │ ├── Generator.php │ ├── Node.php │ └── Action.php │ ├── Http │ ├── Response │ │ ├── TextResponse.php │ │ ├── HtmlResponse.php │ │ ├── XmlResponse.php │ │ ├── RedirectResponse.php │ │ └── JsonResponse.php │ ├── Exception │ │ ├── NotFoundException.php │ │ ├── ForbiddenException.php │ │ ├── UnauthorizedException.php │ │ ├── BadRequestException.php │ │ ├── InvalidResponseFormatException.php │ │ └── HttpException.php │ ├── Request │ │ └── AuthorizationAware.php │ ├── Cookie.php │ ├── ServerBag.php │ └── HeaderBag.php │ ├── Annotation │ ├── InvalidAnnotationException.php │ ├── BaseRoute.php │ ├── BaseURL.php │ ├── Validate.php │ ├── Route.php │ ├── RespondsWith.php │ ├── RequiresLevel.php │ ├── Schedule.php │ └── ArrayArguments.php │ ├── DB │ ├── Exception.php │ ├── Exception │ │ ├── ConnectionException.php │ │ ├── DuplicateKeyException.php │ │ ├── SyntaxErrorException.php │ │ ├── TableExistsException.php │ │ ├── TableNotFoundException.php │ │ ├── InvalidFieldNameException.php │ │ ├── NonUniqueFieldNameException.php │ │ ├── NotNullConstraintViolationException.php │ │ └── ForeignKeyConstraintViolationException.php │ ├── Driver │ │ ├── DriverInterface.php │ │ └── AbstractDriver.php │ ├── Generator │ │ └── Templates │ │ │ ├── stub-model.phpt │ │ │ ├── relations │ │ │ ├── one-to-one.phpt │ │ │ ├── many-to-one.phpt │ │ │ ├── one-to-many.phpt │ │ │ └── many-to-many.phpt │ │ │ └── base-model.phpt │ ├── Statement.php │ ├── Constraint.php │ ├── Connection.php │ ├── Table.php │ ├── Generator.php │ └── Column.php │ ├── Validator │ ├── CleanerInterface.php │ ├── Exception │ │ ├── ValidationException.php │ │ ├── InvalidConstraintException.php │ │ └── InvalidInputException.php │ ├── Datatypes │ │ ├── StringDatatype.php │ │ ├── ArrayDatatype.php │ │ ├── DomainDatatype.php │ │ ├── AlphanumDatatype.php │ │ ├── AbstractDatatype.php │ │ ├── UrlDatatype.php │ │ ├── EmailDatatype.php │ │ ├── IntDatatype.php │ │ ├── FloatDatatype.php │ │ └── BooleanDatatype.php │ ├── Constraints │ │ ├── DefaultConstraint.php │ │ ├── InstanceOfConstraint.php │ │ ├── EqualsConstraint.php │ │ ├── RenameConstraint.php │ │ ├── AllowNullConstraint.php │ │ ├── UniqueConstraint.php │ │ ├── MemberOfConstraint.php │ │ ├── DependsOnConstraint.php │ │ ├── TransformConstraint.php │ │ ├── RegexConstraint.php │ │ ├── PropertyEqualsConstraint.php │ │ ├── MinLengthConstraint.php │ │ ├── MaxLengthConstraint.php │ │ ├── CallableConstraint.php │ │ ├── DateConstraint.php │ │ ├── MapConstraint.php │ │ ├── AbstractConstraint.php │ │ ├── ExistsConstraint.php │ │ ├── AbstractLengthConstraint.php │ │ ├── TypeConstraint.php │ │ └── AnyConstraint.php │ └── Result.php │ ├── Config │ ├── UnknownConfigOptionException.php │ └── Row.php │ ├── View │ ├── TwigExtension.php │ ├── TwigEnvironment.php │ └── Tag │ │ ├── Output.php │ │ ├── Img.php │ │ └── Register.php │ ├── IAuthable.php │ ├── Method.php │ ├── Log │ ├── Cli.php │ ├── CliHandler.php │ └── ExceptionIntrospectionProcessor.php │ ├── Storage │ └── Cookie.php │ ├── IResource.php │ ├── Registry.php │ ├── Core.php │ ├── Cache.php │ ├── Hook.php │ ├── Utils │ ├── JSON.php │ └── XML.php │ ├── Utils.php │ ├── Config.php │ ├── Annotation.php │ ├── Auth.php │ ├── Inflector.php │ ├── Log.php │ ├── View.php │ ├── Router.php │ ├── Reflector.php │ └── Exception.php ├── bin ├── schedule │ ├── run │ └── print ├── wave ├── generate │ ├── views │ ├── routes │ └── models ├── cli-tools.php └── install ├── LICENSE └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | composer.lock 3 | vendor/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | wave 2 | ==== 3 | 4 | Wave is a lightweight MVC framework written in PHP. 5 | -------------------------------------------------------------------------------- /src/Wave/Storage.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Wave/Router/RoutingException.php: -------------------------------------------------------------------------------- 1 | input); 10 | } 11 | 12 | /** 13 | * @return string a type to use in the violation message 14 | */ 15 | public function getType() { 16 | return 'string'; 17 | } 18 | } 19 | 20 | 21 | ?> -------------------------------------------------------------------------------- /src/Wave/Validator/Datatypes/ArrayDatatype.php: -------------------------------------------------------------------------------- 1 | input); 10 | 11 | } 12 | 13 | 14 | /** 15 | * @return string a type to use in the violation message 16 | */ 17 | public function getType() { 18 | return 'array'; 19 | } 20 | } 21 | 22 | 23 | ?> -------------------------------------------------------------------------------- /src/Wave/View/TwigExtension.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Wave/Http/Response/HtmlResponse.php: -------------------------------------------------------------------------------- 1 | headers->set('Content-Type', 'text/html; charset=utf8'); 15 | $this->headers->set('X-Wave-Response', 'html'); 16 | 17 | return $this; 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /src/Wave/IAuthable.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Wave/DB/Driver/DriverInterface.php: -------------------------------------------------------------------------------- 1 | input) > 0; 9 | } 10 | 11 | /** 12 | * @return string a type to use in the violation message 13 | */ 14 | public function getType() { 15 | return 'domain'; 16 | } 17 | } 18 | 19 | 20 | ?> -------------------------------------------------------------------------------- /src/Wave/Validator/Exception/InvalidInputException.php: -------------------------------------------------------------------------------- 1 | violations = $violations; 11 | 12 | parent::__construct('Input validation failed', 400); 13 | } 14 | 15 | public function getViolations() { 16 | return $this->violations; 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/Wave/Validator/Datatypes/AlphanumDatatype.php: -------------------------------------------------------------------------------- 1 | input)) return false; 10 | return preg_match('/^[A-Za-z0-9]*$/', $this->input) > 0; 11 | } 12 | 13 | 14 | /** 15 | * @return string a type to use in the violation message 16 | */ 17 | public function getType() { 18 | return 'alphanumeric string'; 19 | } 20 | } 21 | 22 | 23 | ?> -------------------------------------------------------------------------------- /src/Wave/Annotation/BaseRoute.php: -------------------------------------------------------------------------------- 1 | minimumParameterCount(1); 13 | $this->maximumParameterCount(1); 14 | $this->validOnSubclassesOf($class, Annotation::CLASS_CONTROLLER); 15 | } 16 | 17 | public function apply(Action &$action) { 18 | $action->addBaseRoute($this->parameters[0]); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Wave/Annotation/BaseURL.php: -------------------------------------------------------------------------------- 1 | minimumParameterCount(1); 15 | $this->maximumParameterCount(1); 16 | $this->validOnSubclassesOf($class, Annotation::CLASS_CONTROLLER); 17 | } 18 | 19 | public function apply(Action &$action) { 20 | $action->setProfile($this->parameters[0]); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/Wave/Method.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Wave/Validator/Datatypes/AbstractDatatype.php: -------------------------------------------------------------------------------- 1 | input = $input; 14 | } 15 | 16 | /** 17 | * @param mixed $input 18 | * @return bool 19 | */ 20 | abstract public function __invoke(); 21 | 22 | /** 23 | * @return string a type to use in the violation message 24 | */ 25 | abstract public function getType(); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/Wave/Validator/Datatypes/UrlDatatype.php: -------------------------------------------------------------------------------- 1 | cleaned = trim($this->input); 11 | return filter_var($this->cleaned, FILTER_VALIDATE_URL); 12 | } 13 | 14 | public function getCleanedData() { 15 | return $this->cleaned; 16 | } 17 | 18 | /** 19 | * @return string a type to use in the violation message 20 | */ 21 | public function getType() { 22 | return 'url'; 23 | } 24 | } -------------------------------------------------------------------------------- /bin/schedule/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getScheduledRoutes(); 11 | 12 | foreach ($scheduled_routes as $action) { 13 | $expression = new \Cron\CronExpression($action->getSchedule()); 14 | if ($expression->isDue('now', $action->getScheduleTimezone())) { 15 | printf("Running %s\n", $action->getAction()); 16 | $response = Controller::invoke($action, new Request('schedule://x-wave')); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/DefaultConstraint.php: -------------------------------------------------------------------------------- 1 | validator->hasInputData($this->property) === null ? $this->arguments : $this->data; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/InstanceOfConstraint.php: -------------------------------------------------------------------------------- 1 | data instanceof $this->arguments; 15 | } 16 | 17 | protected function getViolationMessage($context = 'This value') { 18 | return sprintf("%s is not a valid %s object", $context, $this->arguments); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Wave/Annotation/Validate.php: -------------------------------------------------------------------------------- 1 | exactParameterCount(1); 14 | $this->validOnSubclassesOf($class, Annotation::CLASS_CONTROLLER); 15 | } 16 | 17 | public function build(){ 18 | $this->schema = $this->parameters[0]; 19 | } 20 | 21 | public function apply(Action &$action){ 22 | $action->setValidationSchema($this->schema); 23 | } 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/Wave/DB/Generator/Templates/stub-model.phpt: -------------------------------------------------------------------------------- 1 | cleaned = trim($this->input); 11 | return filter_var($this->cleaned, FILTER_VALIDATE_EMAIL); 12 | } 13 | 14 | public function getCleanedData() { 15 | return $this->cleaned; 16 | } 17 | 18 | /** 19 | * @return string a type to use in the violation message 20 | */ 21 | public function getType() { 22 | return 'email'; 23 | } 24 | } 25 | 26 | 27 | ?> -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/EqualsConstraint.php: -------------------------------------------------------------------------------- 1 | data === $this->arguments; 16 | } 17 | 18 | protected function getViolationMessage($context = 'This value') { 19 | return sprintf("%s does not equal %s", $context, $this->arguments); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /bin/wave: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Proxy script for executing wave bin scripts. Adapted from a 4 | # script by Steven Gilberd (steve@erayd.net) 5 | # @author Patrick Hindmarsh patrick@hindmar.sh 6 | 7 | # Get the real directory of this script to prefix the command with 8 | # http://stackoverflow.com/questions/59895 9 | SOURCE="${BASH_SOURCE[0]}" 10 | DIR="$( dirname "$SOURCE" )" 11 | while [ -h "$SOURCE" ] 12 | do 13 | SOURCE="$(readlink "$SOURCE")" 14 | [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" 15 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 16 | done 17 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 18 | 19 | TOOL="$DIR/$1" 20 | shift 21 | exec $TOOL $@ 22 | -------------------------------------------------------------------------------- /src/Wave/Validator/Datatypes/IntDatatype.php: -------------------------------------------------------------------------------- 1 | input) || (is_string($this->input) && strval(intval($this->input)) === $this->input)); 11 | } 12 | 13 | public function getCleanedData() { 14 | return intval($this->input); 15 | } 16 | 17 | /** 18 | * @return string a type to use in the violation message 19 | */ 20 | public function getType() { 21 | return 'integer'; 22 | } 23 | } 24 | 25 | 26 | ?> -------------------------------------------------------------------------------- /src/Wave/Http/Exception/HttpException.php: -------------------------------------------------------------------------------- 1 | request = $request; 19 | $this->response = $response; 20 | 21 | parent::__construct($message, $this->getStatusCode()); 22 | 23 | } 24 | 25 | protected function getStatusCode() { 26 | return Response::STATUS_SERVER_ERROR; 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/Wave/Log/Cli.php: -------------------------------------------------------------------------------- 1 | setFormatter(new LineFormatter(CliHandler::LINE_FORMAT)); 16 | 17 | $channel_handler = parent::createChannel($channel, $handler); 18 | $channel_handler->pushHandler($cli_handler); 19 | 20 | static::setChannel($channel, $channel_handler); 21 | 22 | return $channel_handler; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Wave/Http/Request/AuthorizationAware.php: -------------------------------------------------------------------------------- 1 | minimumParameterCount(2); 15 | $this->maximumParameterCount(2); 16 | $this->validOnSubclassesOf($class, Annotation::CLASS_CONTROLLER); 17 | } 18 | 19 | public function build(){ 20 | $this->methods = explode('|', $this->parameters[0]); 21 | $this->url = $this->parameters[1]; 22 | } 23 | 24 | public function apply(Action &$action){ 25 | $action->addRoute($this->methods, $this->url); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/Wave/View/TwigEnvironment.php: -------------------------------------------------------------------------------- 1 | array(), 'js' => array()); 10 | 11 | public function _wave_register($type, $path, $extras = null, $priority = 0, $cache_key = null) { 12 | if(is_string($cache_key)) { 13 | if(strpos($path, '?') !== false) $path .= '&'; 14 | else $path .= '?'; 15 | $path .= 'v=' . $cache_key; 16 | } 17 | 18 | $this->_wave_register[$type][$priority][$path] = $extras; 19 | 20 | krsort($this->_wave_register[$type], SORT_NUMERIC); 21 | } 22 | 23 | 24 | } 25 | 26 | 27 | ?> -------------------------------------------------------------------------------- /src/Wave/Storage/Cookie.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Wave/Validator/Result.php: -------------------------------------------------------------------------------- 1 | violations = $errors; 18 | $this->validator = $validator; 19 | } 20 | 21 | public function isValid() { 22 | return empty($this->violations); 23 | } 24 | 25 | public function getViolations() { 26 | return $this->violations; 27 | } 28 | 29 | public function getCleanedData() { 30 | return $this->getArrayCopy(); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/RenameConstraint.php: -------------------------------------------------------------------------------- 1 | validator->setCleanedData($this->arguments, $this->data); 23 | $this->validator->unsetCleanedData($this->property, true); 24 | return true; 25 | } 26 | 27 | 28 | } -------------------------------------------------------------------------------- /src/Wave/Validator/Datatypes/FloatDatatype.php: -------------------------------------------------------------------------------- 1 | input)) return false; 12 | 13 | return false !== filter_var($this->input, FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND); 14 | } 15 | 16 | public function getCleanedData() { 17 | return floatval(filter_var($this->input, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION)); 18 | } 19 | 20 | /** 21 | * @return string a type to use in the violation message 22 | */ 23 | public function getType() { 24 | return 'decimal number'; 25 | } 26 | } 27 | 28 | 29 | ?> -------------------------------------------------------------------------------- /src/Wave/IResource.php: -------------------------------------------------------------------------------- 1 | @int 25 | **/ 26 | public function get(); 27 | 28 | /** 29 | * ~Route POST /entity/@int 30 | **/ 31 | public function update(); 32 | 33 | /** 34 | * ~Route POST /entity/@int/delete 35 | **/ 36 | public function delete(); 37 | 38 | } 39 | 40 | 41 | ?> -------------------------------------------------------------------------------- /src/Wave/Registry.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/schedule/print: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getScheduledRoutes(); 7 | 8 | $format = "| %-50s | %-28s | %-17s | %-33s |"; 9 | $header = sprintf($format, 'Action', 'Schedule', 'Timezone', 'Next Run'); 10 | $break = str_repeat('-', strlen($header)); 11 | 12 | print "$break\n"; 13 | print "$header\n"; 14 | print "$break\n"; 15 | 16 | foreach ($scheduled_routes as $action) { 17 | $expression = new \Cron\CronExpression($action->getSchedule()); 18 | $next_run = $expression->getNextRunDate('now', 0, false, $action->getScheduleTimezone()); 19 | printf($format, $action->getAction(), $action->getSchedule(), $next_run->getTimezone()->getName(), $next_run->format('r')); 20 | print "\n"; 21 | } 22 | 23 | print "$break\n"; -------------------------------------------------------------------------------- /src/Wave/Annotation/RespondsWith.php: -------------------------------------------------------------------------------- 1 | minimumParameterCount(1); 16 | $this->validOnSubclassesOf($class, Annotation::CLASS_CONTROLLER); 17 | } 18 | 19 | 20 | public function build(){ 21 | $this->inherit = false; 22 | if(isset($this->parameters['inherit'])){ 23 | $this->inherit = $this->parameters['inherit'] == 'true'; 24 | unset($this->parameters['inherit']); 25 | } 26 | 27 | $this->methods = $this->parameters; 28 | } 29 | 30 | public function apply(Action &$action){ 31 | $action->setRespondsWith($this->methods, $this->inherit); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/AllowNullConstraint.php: -------------------------------------------------------------------------------- 1 | data === null) { 19 | // we return false here but with no violations 20 | // to stop execution of subsequent constraints 21 | return false; 22 | } 23 | return true; 24 | 25 | } 26 | 27 | public function getViolationPayload() { 28 | return array(); 29 | } 30 | 31 | 32 | public function getCleanedData() { 33 | return $this->data; 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/Wave/Core.php: -------------------------------------------------------------------------------- 1 | mode; 22 | 23 | Debug::init($config['debug']); 24 | 25 | self::$_MODE = $config['mode']; 26 | self::setErrorReporting($config['mode'] !== self::MODE_PRODUCTION); 27 | 28 | Cache::init(); 29 | 30 | Hook::triggerAction('bootstrapped'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/UniqueConstraint.php: -------------------------------------------------------------------------------- 1 | data; 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | protected function getViolationKey() { 29 | return static::ERROR_NOT_UNIQUE; 30 | } 31 | 32 | protected function getViolationMessage($context = 'This value') { 33 | $message = isset($this->message) ? $this->message : '%s is not unique'; 34 | return sprintf($message, $context); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/MemberOfConstraint.php: -------------------------------------------------------------------------------- 1 | data, $this->arguments, true); 15 | } 16 | 17 | /** 18 | * @return string 19 | */ 20 | protected function getViolationKey() { 21 | return static::ERROR_NOT_MEMBER; 22 | } 23 | 24 | protected function getViolationMessage($context = 'This value') { 25 | return sprintf('%s is not a valid choice', $context); 26 | } 27 | 28 | public function getViolationPayload() { 29 | return array_merge( 30 | parent::getViolationPayload(), 31 | array( 32 | 'member_of' => $this->arguments 33 | ) 34 | ); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/Wave/DB/Statement.php: -------------------------------------------------------------------------------- 1 | setFetchMode(\PDO::FETCH_ASSOC); 15 | $this->connection = $connection; 16 | 17 | } 18 | 19 | public function execute($input_parameters = null): bool { 20 | 21 | $start = microtime(true); 22 | $result = parent::execute($input_parameters); 23 | 24 | $query_data = [ 25 | 'query' => $this->queryString, 26 | 'row_count' => $this->rowCount(), 27 | 'success' => $this->errorCode() === \PDO::ERR_NONE, 28 | 'time' => microtime(true) - $start, 29 | 'params' => $input_parameters 30 | ]; 31 | 32 | Hook::triggerAction('db.after_query', [$query_data]); 33 | 34 | return $result; 35 | } 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/Wave/Annotation/RequiresLevel.php: -------------------------------------------------------------------------------- 1 | minimumParameterCount(1); 25 | $this->validOnSubclassesOf($class, Annotation::CLASS_CONTROLLER); 26 | } 27 | 28 | public function build(){ 29 | $this->inherit = true; 30 | if(isset($this->parameters['inherit'])){ 31 | $this->inherit = $this->parameters['inherit'] == 'true'; 32 | unset($this->parameters['inherit']); 33 | } 34 | $this->methods = $this->parameters; 35 | } 36 | 37 | public function apply(Action &$action){ 38 | return $action->setRequiresLevel($this->methods, $this->inherit); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /bin/generate/views: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | path->views); 12 | $iterator = new RecursiveIteratorIterator($dir_iterator); 13 | $c = 0; 14 | 15 | echo "\nResult: \n"; 16 | 17 | echo " Compiled views from : " . \Wave\Config::get('wave')->path->views . "\n\n"; 18 | printf(" %s\n", "File"); 19 | $l = strlen(\Wave\Config::get('wave')->path->views); 20 | echo " " . str_repeat('-', 120) . "\n"; 21 | foreach($iterator as $template) { 22 | $i = pathinfo($template); 23 | if($i['extension'] != 'phtml') continue; 24 | $c++; 25 | printf(" %s\n", substr($template, $l)); 26 | } 27 | echo "\n"; 28 | echo " Total: " . $c . " templates\n"; 29 | echo "\n\n"; 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/DependsOnConstraint.php: -------------------------------------------------------------------------------- 1 | arguments)) $this->arguments = array($this->arguments); 17 | foreach($this->arguments as $field) { 18 | if($this->validator->getSchemaKey($field) === null) 19 | throw new InvalidArgumentException("Can't depend_on '{$field}' since it doesn't exist in schema"); 20 | 21 | if(!$this->validator->offsetExists($field) || $this->validator->getViolation($field) !== null) { 22 | return false; 23 | } 24 | 25 | } 26 | return true; 27 | } 28 | 29 | public function getViolationPayload() { 30 | return array(); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/Wave/Validator/Datatypes/BooleanDatatype.php: -------------------------------------------------------------------------------- 1 | input, $this->bool_true, true)) 16 | $this->converted = true; 17 | else if(in_array($this->input, $this->bool_false, true)) 18 | $this->converted = false; 19 | else 20 | return false; 21 | 22 | return true; 23 | } 24 | 25 | public function getCleanedData() { 26 | return $this->converted; 27 | } 28 | 29 | /** 30 | * @return string a type to use in the violation message 31 | */ 32 | public function getType() { 33 | return 'boolean'; 34 | } 35 | } 36 | 37 | 38 | ?> -------------------------------------------------------------------------------- /src/Wave/Http/Response/XmlResponse.php: -------------------------------------------------------------------------------- 1 | headers->set('Cache-Control', 'no-cache, must-revalidate'); 17 | $this->headers->set('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT'); 18 | $this->headers->set('Content-Type', 'text/xml; charset=utf-8'); 19 | $this->headers->set('X-Wave-Response', 'xml'); 20 | 21 | return $this; 22 | } 23 | 24 | public function setContent($data, $convert = true) { 25 | if($convert) { 26 | $this->data = $data; 27 | $data = XML::encode($data); 28 | } 29 | 30 | parent::setContent($data); 31 | } 32 | 33 | /** 34 | * @return mixed 35 | */ 36 | public function getData() { 37 | return $this->data; 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/Wave/DB/Generator/Templates/relations/one-to-one.phpt: -------------------------------------------------------------------------------- 1 | {# _setRelationObject('{{ relation.Name }}', $obj, $create_relation); 14 | return $this; 15 | } 16 | 17 | /** 18 | * {{ relation.Name }} - one-to-one 19 | * 20 | * @param callable $transform_callback 21 | * @return {{ relation.ReferencedTable.getClassName(true) }} 22 | **/ 23 | public function get{{ relation.Name }}() 24 | { 25 | $transform_callback = func_num_args() >= 1 ? func_get_arg(0) : null; 26 | return $this->_getRelationObjects('{{ relation.Name }}', $transform_callback); 27 | } -------------------------------------------------------------------------------- /src/Wave/DB/Generator/Templates/relations/many-to-one.phpt: -------------------------------------------------------------------------------- 1 | {# _setRelationObject('{{ relation.Name }}', $object, $create_relation); 14 | return $this; 15 | } 16 | 17 | /** 18 | * {{ relation.Name }} - many-to-one 19 | * 20 | * @param callable $transform_callback 21 | * @return {{ relation.ReferencedTable.getClassName(true) }} 22 | **/ 23 | public function get{{ relation.Name }}() 24 | { 25 | $transform_callback = func_num_args() >= 1 ? func_get_arg(0) : null; 26 | return $this->_getRelationObjects('{{ relation.Name }}', $transform_callback); 27 | } -------------------------------------------------------------------------------- /bin/generate/routes: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | path->controllers); 14 | $reflected_options = $reflector->execute(); 15 | $all_routes = \Wave\Router\Generator::buildRoutes($reflected_options); 16 | 17 | // do some sorting based on the url to make display pretty 18 | foreach($all_routes as $dom => $routes) { 19 | echo " Base Route: " . $dom . "\n\n"; 20 | printf(" %-70s %-20s %s \n", "Method", "Respondswith", " URL"); 21 | echo " " . str_repeat('-', 120) . "\n"; 22 | foreach($routes as $route) { 23 | foreach($route->getRoutes() as $url) { 24 | printf(" %-70s %-20s | %s \n", $route->getAction(), implode(', ', $route->getRespondsWith()), $url); 25 | } 26 | } 27 | } 28 | 29 | echo "\n\n"; 30 | } -------------------------------------------------------------------------------- /src/Wave/Config/Row.php: -------------------------------------------------------------------------------- 1 | $value) { 12 | if(is_array($value)) { 13 | $value = new static($value); 14 | } 15 | $this->offsetSet($key, $value); 16 | } 17 | 18 | } 19 | 20 | public function __get($property) { 21 | if(isset($this[$property])) { 22 | return $this[$property]; 23 | } 24 | throw new UnknownConfigOptionException("Unknown config option [$property]"); 25 | } 26 | 27 | public function __isset($property) { 28 | return isset($this[$property]); 29 | } 30 | 31 | public function getArrayCopy(): array { 32 | $self = parent::getArrayCopy(); 33 | 34 | foreach($self as $key => $value){ 35 | if($value instanceof Row){ 36 | $self[$key] = $value->getArrayCopy(); 37 | } 38 | } 39 | return $self; 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 wave-framework 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wave-framework/wave", 3 | "type": "library", 4 | "description": "Wave is a lightweight PHP MVC framework", 5 | "keywords": [ 6 | "framework", 7 | "mvc", 8 | "orm", 9 | "lightweight", 10 | "controllers", 11 | "models", 12 | "views" 13 | ], 14 | "homepage": "http://github.com/wave-framework/wave", 15 | 16 | "authors": [{ 17 | "name": "Patrick Hindmarsh", 18 | "email": "patrick@hindmar.sh", 19 | "homepage": "https://github.com/phindmarsh" 20 | }, 21 | { 22 | "name": "Michael Calcinai", 23 | "email": "michael@calcin.ai" 24 | } 25 | ], 26 | 27 | "bin": ["bin/wave"], 28 | 29 | "require": { 30 | "php": ">=8.1", 31 | "twig/twig": "3.*", 32 | "monolog/monolog": "2.*", 33 | "dragonmantank/cron-expression": "^3.3" 34 | }, 35 | 36 | "require-dev": { 37 | "phpunit/phpunit": "3.7.*" 38 | }, 39 | 40 | "autoload": { 41 | "psr-0": { 42 | "PostInstall": "bin/composer", 43 | "Wave": "src" 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/TransformConstraint.php: -------------------------------------------------------------------------------- 1 | data = call_user_func_array( 29 | $this->arguments, array( 30 | &$this->data, 31 | &$this->validator, 32 | &$this->key, 33 | &$this->message, 34 | ) 35 | ); 36 | 37 | return true; 38 | } 39 | 40 | 41 | public function getCleanedData() { 42 | return $this->data; 43 | } 44 | } -------------------------------------------------------------------------------- /src/Wave/Http/Response/RedirectResponse.php: -------------------------------------------------------------------------------- 1 | setTarget($url); 16 | } 17 | 18 | public function getTarget() { 19 | return $this->target; 20 | } 21 | 22 | public function setTarget($target) { 23 | $this->target = $target; 24 | 25 | $this->setContent( 26 | sprintf( 27 | ' 28 | 29 | 30 | 31 | 32 | 33 | Redirecting to %1$s 34 | 35 | 36 | Redirecting to %1$s. 37 | 38 | ', htmlspecialchars($target, ENT_QUOTES, 'UTF-8') 39 | ) 40 | ); 41 | 42 | $this->headers->set('Location', $target); 43 | } 44 | 45 | 46 | } -------------------------------------------------------------------------------- /src/Wave/Cache.php: -------------------------------------------------------------------------------- 1 | path->cache; 12 | self::$_ready = true; 13 | } 14 | 15 | public static function load($key) { 16 | $filename = self::$_cachepath . $key; 17 | 18 | if(file_exists($filename)) 19 | return unserialize(file_get_contents(self::$_cachepath . $key)); 20 | else 21 | return null; 22 | } 23 | 24 | public static function store($key, $data) { 25 | $path = self::$_cachepath . $key; 26 | $dir = dirname($path); 27 | if(!is_dir($dir)) 28 | @mkdir($dir, 0770, true); 29 | file_put_contents($path, serialize($data)); 30 | } 31 | 32 | public static function delete($key) { 33 | @unlink(self::$_cachepath . $key); 34 | } 35 | 36 | public static function cachetime($key) { 37 | $filename = self::$_cachepath . $key; 38 | if(file_exists($filename)) 39 | return filemtime(self::$_cachepath . $key); 40 | else 41 | return 0; 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /bin/cli-tools.php: -------------------------------------------------------------------------------- 1 | getConnection()->getDriverClass(); 22 | 23 | $tables = $driver_class::getTables($database); 24 | $columns = $driver_class::getColumns($database); 25 | $keys = $driver_class::getColumnKeys($database); 26 | 27 | echo " Alias: " . $database->getNamespace() . "\n"; 28 | echo " Driver: " . $driver_class . "\n"; 29 | echo " Tables (" . count($tables) . "): \n"; 30 | printf(" %-40s %-10s %s \n", "Name", "Engine", "Comments"); 31 | echo " " . str_repeat('-', 120) . "\n"; 32 | foreach($tables as $table) { 33 | printf(" %-40s %-10s %s \n", $table['table_name'], $table['table_engine'], $table['table_comment']); 34 | } 35 | } 36 | echo "\n\n"; 37 | } 38 | -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/RegexConstraint.php: -------------------------------------------------------------------------------- 1 | message = $arguments['message']; 17 | $this->arguments = $arguments['pattern']; 18 | } else throw new \InvalidArgumentException("Invalid format for regex constraint, must contain a [message] and [pattern]"); 19 | } 20 | 21 | } 22 | 23 | /** 24 | * Evaluate the current constraint against the schema arguments and input data. 25 | * 26 | * @return mixed 27 | */ 28 | public function evaluate() { 29 | return preg_match($this->arguments, $this->data) > 0; 30 | } 31 | 32 | protected function getViolationMessage($context = 'This value') { 33 | if(isset($this->message)) { 34 | return sprintf($this->message, $context); 35 | } else return parent::getViolationMessage($context); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/PropertyEqualsConstraint.php: -------------------------------------------------------------------------------- 1 | data)) 21 | throw new \InvalidArgumentException("[property_equals] constraint requires an object parameter"); 22 | } 23 | 24 | /** 25 | * Evaluate the current constraint against the schema arguments and input data. 26 | * 27 | * @return mixed 28 | */ 29 | public function evaluate() { 30 | $property = $this->arguments['property']; 31 | return isset($this->data->$property) && $this->data->$property === $this->arguments['value']; 32 | } 33 | 34 | protected function getViolationMessage($context = 'This value') { 35 | return sprintf($this->arguments['message'], $context); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/Wave/Hook.php: -------------------------------------------------------------------------------- 1 | addCheckpoint($action); 31 | if(isset(self::$handlers[$action])) { 32 | foreach(self::$handlers[$action] as $priority => $handlers) { 33 | foreach($handlers as $handler) { 34 | if(is_callable($handler)) { 35 | call_user_func_array($handler, $data); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/MinLengthConstraint.php: -------------------------------------------------------------------------------- 1 | count >= $this->limit; 15 | } 16 | 17 | /** 18 | * @return string 19 | */ 20 | protected function getViolationKey() { 21 | return static::ERROR_TOO_SHORT; 22 | } 23 | 24 | protected function getViolationMessage($context = 'This value') { 25 | if($this->message !== null) 26 | return $this->message; 27 | else if($this->comparator === static::COMPARATOR_ARRAY) 28 | return sprintf('%s must have at least %s members', $context, $this->limit); 29 | elseif($this->comparator === static::COMPARATOR_INT) 30 | return sprintf('%s must be greater than %s', $context, $this->limit); 31 | elseif($this->comparator === static::COMPARATOR_DATETIME) 32 | return sprintf('%s must be after %s', $context, $this->limit->format('c')); 33 | else 34 | return sprintf('%s must be at least %s characters', $context, $this->limit); 35 | } 36 | 37 | public function getViolationPayload() { 38 | return array_merge( 39 | parent::getViolationPayload(), 40 | array( 41 | 'min_length' => $this->limit 42 | ) 43 | ); 44 | } 45 | 46 | 47 | } -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/MaxLengthConstraint.php: -------------------------------------------------------------------------------- 1 | count <= $this->limit; 15 | } 16 | 17 | /** 18 | * @return string 19 | */ 20 | protected function getViolationKey() { 21 | return static::ERROR_TOO_LONG; 22 | } 23 | 24 | protected function getViolationMessage($context = 'This value') { 25 | if($this->message !== null) 26 | return $this->message; 27 | else if($this->comparator === static::COMPARATOR_ARRAY) 28 | return sprintf('%s must have no more than %s members', $context, $this->limit); 29 | elseif($this->comparator === static::COMPARATOR_INT) 30 | return sprintf('%s must be less than %s', $context, $this->limit); 31 | elseif($this->comparator === static::COMPARATOR_DATETIME) 32 | return sprintf('%s must be before %s', $context, $this->limit->format('c')); 33 | else 34 | return sprintf('%s must have no more than %s characters', $context, $this->limit); 35 | } 36 | 37 | public function getViolationPayload() { 38 | return array_merge( 39 | parent::getViolationPayload(), 40 | array( 41 | 'max_length' => $this->limit 42 | ) 43 | ); 44 | } 45 | 46 | 47 | } -------------------------------------------------------------------------------- /src/Wave/Utils/JSON.php: -------------------------------------------------------------------------------- 1 | format(DateTime::ATOM); 23 | } else if(is_array($data) || is_object($data)) { 24 | $jsonarr = array(); 25 | 26 | if($data instanceof Model) { 27 | $data = $data->_toArray(); 28 | } 29 | 30 | if($data instanceof ArrayObject){ 31 | $data = $data->getArrayCopy(); 32 | } 33 | 34 | foreach($data as $key => $value) { 35 | $jsonarr[$key] = self::arrayify($value); 36 | } 37 | 38 | // empty objects will be converted to arrays when json_encoded 39 | // so preserve the object type if the array is empty 40 | if(is_object($data) && empty($jsonarr)){ 41 | return (object) $jsonarr; 42 | } 43 | else { 44 | return $jsonarr; 45 | } 46 | } else { 47 | return $data; 48 | } 49 | } 50 | } 51 | 52 | ?> 53 | -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/CallableConstraint.php: -------------------------------------------------------------------------------- 1 | cleaned = $this->data; 25 | } 26 | 27 | /** 28 | * @return bool 29 | */ 30 | public function evaluate() { 31 | return call_user_func_array( 32 | $this->arguments, array( 33 | &$this->data, 34 | &$this->validator, 35 | &$this->key, 36 | &$this->message, 37 | ) 38 | ); 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | protected function getViolationKey() { 45 | return $this->key; 46 | } 47 | 48 | protected function getViolationMessage($context = 'This value') { 49 | return sprintf($this->message, $context); 50 | } 51 | 52 | public function getCleanedData() { 53 | return $this->data; 54 | } 55 | } -------------------------------------------------------------------------------- /src/Wave/DB/Generator/Templates/relations/one-to-many.phpt: -------------------------------------------------------------------------------- 1 | {# _addRelationObject('{{ relation.Name }}', $object, $create_relation); 14 | return $this; 15 | } 16 | 17 | /** 18 | * {{ relation.Name }} - one-to-many 19 | * 20 | * @param {{ relation.ReferencedTable.getClassName(true) }} $obj The {{ relation.Name }} object to be removed 21 | * @param bool $delete_object whether to delete the object in the database 22 | * @return $this 23 | **/ 24 | public function remove{{ relation.Name|singularize }}({{ relation.ReferencedTable.getClassName(true) }} $object, $delete_object = true) 25 | { 26 | $this->_removeRelationObject('{{ relation.Name }}', $object, $delete_object); 27 | return $this; 28 | } 29 | 30 | /** 31 | * {{ relation.Name }} - one-to-many 32 | * 33 | * @param callable $transform_callback 34 | * @return {{ relation.ReferencedTable.getClassName(true) }}[] 35 | **/ 36 | public function get{{ relation.Name }}() 37 | { 38 | $transform_callback = func_num_args() >= 1 ? func_get_arg(0) : null; 39 | return $this->_getRelationObjects('{{ relation.Name }}', $transform_callback); 40 | } -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/DateConstraint.php: -------------------------------------------------------------------------------- 1 | format = $arguments; 19 | 20 | } 21 | 22 | /** 23 | * Evaluate the current constraint against the schema arguments and input data. 24 | * 25 | * @return mixed 26 | */ 27 | public function evaluate() { 28 | 29 | $this->datetime = null; 30 | 31 | try { 32 | if($this->format !== null) 33 | $this->datetime = DateTime::createFromFormat($this->format, $this->data); 34 | else 35 | $this->datetime = new DateTime($this->data); 36 | 37 | if(!($this->datetime instanceof DateTime)) return false; 38 | return true; 39 | 40 | } catch(\Exception $e) { 41 | return false; 42 | } 43 | 44 | } 45 | 46 | protected function getViolationMessage($context = 'This date') { 47 | if($this->format === null) 48 | return sprintf('%s is not in a recognised date format', $context); 49 | else 50 | return sprintf('%s is not in the required format (%s)', $context, $this->format); 51 | } 52 | 53 | public function getCleanedData() { 54 | return $this->datetime; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Wave/Utils.php: -------------------------------------------------------------------------------- 1 | profiles->$profile; 22 | $domain = $conf->baseurl; 23 | $protocol = 'http'; 24 | if(isset($conf->ssl) && $conf->ssl == true) 25 | $protocol .= 's'; 26 | $uri = $protocol . '://' . $domain . $uri; 27 | } 28 | 29 | header('Location: ' . $uri); 30 | } 31 | 32 | public static function extractFromObjectArray($key, $objs) { 33 | 34 | $extract = array(); 35 | foreach($objs as $obj) { 36 | if(isset($obj->$key)) 37 | $extract[] = $obj->$key; 38 | } 39 | 40 | return $extract; 41 | } 42 | 43 | public static function shorten($string, $length = 20, $by_word = true, $elipsis = true) { 44 | 45 | if(strlen($string) <= $length) return $string; 46 | 47 | $str = substr($string, 0, $length); 48 | 49 | if($by_word) { 50 | $pos = strrpos($str, ' '); 51 | if($pos !== false) 52 | $str = substr($str, 0, $pos); 53 | } 54 | 55 | if($elipsis) 56 | $str .= '…'; 57 | 58 | return $str; 59 | 60 | } 61 | 62 | } 63 | 64 | 65 | ?> -------------------------------------------------------------------------------- /src/Wave/Annotation/Schedule.php: -------------------------------------------------------------------------------- 1 | minimumParameterCount(1); 24 | $this->maximumParameterCount(2); 25 | $this->validOnSubclassesOf($class, Annotation::CLASS_CONTROLLER); 26 | 27 | if (!CronExpression::isValidExpression($this->parameters[0])) { 28 | throw new InvalidAnnotationException(sprintf('Cron expression [%s] in %s not valid', $this->parameters[0], $class)); 29 | } 30 | } 31 | 32 | public function build() 33 | { 34 | $this->expression = $this->parameters[0]; 35 | if (isset($this->parameters[1])) { 36 | $this->timezone = $this->parameters[1]; 37 | } 38 | } 39 | 40 | public function apply(Action &$action) 41 | { 42 | $action->setSchedule($this->expression); 43 | $action->setScheduleTimezone($this->timezone); 44 | $action->setRespondsWith([Response::FORMAT_CLI], false); 45 | } 46 | 47 | /** 48 | * Overload parameter parsing to allow * / (with a space) 49 | * for a cron expression 50 | * 51 | * @return array 52 | */ 53 | public function parseParameters(): array 54 | { 55 | $parameters = parent::parseParameters(); 56 | $parameters[0] = str_replace('* /', '*/', $parameters[0]); 57 | return $parameters; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Wave/Config.php: -------------------------------------------------------------------------------- 1 | _data = new Row($config_data); 28 | } 29 | 30 | public function __get($offset) { 31 | return $this->_data->{$offset}; 32 | } 33 | 34 | public function offsetExists($offset){ 35 | return isset($this->_data->{$offset}); 36 | } 37 | 38 | /** 39 | * @param $namespace 40 | * @return $this 41 | */ 42 | public static function get($namespace) { 43 | static $configs; 44 | if(!isset($configs[$namespace])) { 45 | $configs[$namespace] = new self($namespace); 46 | } 47 | return $configs[$namespace]; 48 | } 49 | 50 | public static function setResolver(callable $resolver) { 51 | self::$resolver = $resolver; 52 | } 53 | 54 | public static function defaultResolver($namespace) { 55 | $file_path = self::$base_directory . "/$namespace.php"; 56 | 57 | if(!is_readable($file_path)) 58 | throw new \Wave\Exception("Could not load configuration file $namespace. Looked in $file_path"); 59 | 60 | return include $file_path; 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/Wave/DB/Driver/AbstractDriver.php: -------------------------------------------------------------------------------- 1 | format('Y-m-d H:i:s'); 17 | case is_object($value) || is_array($value): 18 | return json_encode($value); 19 | 20 | default: 21 | return $value; 22 | } 23 | } 24 | 25 | public static function valueFromSQL($value, array $field_data) 26 | { 27 | if ($value === null) { 28 | return null; 29 | } else { 30 | if (!is_scalar($value)) { 31 | return $value; 32 | } 33 | } 34 | 35 | switch ($field_data['data_type']) { 36 | 37 | case Column::TYPE_BOOL: 38 | return (bool)$value; 39 | 40 | case Column::TYPE_INT: 41 | return (int)$value; 42 | 43 | case Column::TYPE_FLOAT: 44 | return (float)$value; 45 | 46 | case Column::TYPE_STRING: 47 | return (string)$value; 48 | 49 | case Column::TYPE_JSON: 50 | return json_decode($value); 51 | 52 | case Column::TYPE_DATE: 53 | case Column::TYPE_TIMESTAMP: 54 | if ($value == 'CURRENT_TIMESTAMP') { 55 | $value = 'now'; 56 | } 57 | return new \DateTime($value); 58 | 59 | default: 60 | return $value; 61 | } 62 | } 63 | 64 | } 65 | 66 | 67 | ?> -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/MapConstraint.php: -------------------------------------------------------------------------------- 1 | data)) 22 | throw new \InvalidArgumentException("[map] constraint requires an array of input data"); 23 | } 24 | 25 | /** 26 | * @return bool 27 | */ 28 | public function evaluate() { 29 | 30 | $schema = array('fields' => array($this->property => $this->arguments)); 31 | foreach($this->data as $i => $data) { 32 | $input = array($this->property => $data); 33 | $instance = new Validator($input, $schema, $this->validator); 34 | 35 | if($instance->execute(true)) { 36 | $cleaned = $instance->getCleanedData(); 37 | $this->cleaned[$i] = $cleaned[$this->property]; 38 | } else { 39 | $violations = $instance->getViolations(); 40 | $this->violations = $violations[$this->property]; 41 | return false; 42 | } 43 | } 44 | 45 | return true; 46 | 47 | } 48 | 49 | public function getViolationPayload() { 50 | return $this->violations; 51 | } 52 | 53 | public function getCleanedData() { 54 | return $this->cleaned; 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /src/Wave/View/Tag/Output.php: -------------------------------------------------------------------------------- 1 | getLine(); 14 | 15 | 16 | $type = $this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(); 17 | 18 | 19 | $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); 20 | 21 | return new OutputNode($type, $lineno, $this->getTag()); 22 | } 23 | 24 | public function getTag() { 25 | return 'output'; 26 | } 27 | 28 | 29 | } 30 | 31 | class OutputNode extends Node { 32 | 33 | 34 | const FORMAT_JS = ''; 35 | const FORMAT_CSS = ''; 36 | 37 | public function __construct($type, $line, $tag = null) { 38 | parent::__construct(array(), array('type' => $type), $line, $tag); 39 | } 40 | 41 | public function compile(Compiler $compiler) { 42 | 43 | $compiler 44 | ->addDebugInfo($this); 45 | 46 | $type = $this->getAttribute('type'); 47 | 48 | $template = $type == \Wave\View\Tag\Register::TYPE_JS ? self::FORMAT_JS : self::FORMAT_CSS; 49 | 50 | $compiler->write('foreach($this->env->_wave_register["' . $type . '"] as $priority => $files):')->raw("\n\t") 51 | ->write('foreach($files as $file => $extra):')->raw("\n\t") 52 | ->write('$code = sprintf("' . $template . '", $file, $extra);')->raw("\n\t") 53 | ->write('echo $code . "\n";')->raw("\n\t") 54 | ->write('endforeach;')->raw("\n") 55 | ->write('endforeach;')->raw("\n"); 56 | 57 | 58 | } 59 | 60 | } 61 | 62 | 63 | ?> -------------------------------------------------------------------------------- /src/Wave/DB/Constraint.php: -------------------------------------------------------------------------------- 1 | columns = array($column); 30 | $this->type = $type; 31 | $this->name = $name; 32 | } 33 | 34 | /** 35 | * @param Column $column 36 | */ 37 | public function addColumn(Column $column) { 38 | if(!in_array($column, $this->columns)) 39 | $this->columns[] = $column; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getName() { 46 | return $this->name; 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getType() { 53 | return $this->type; 54 | } 55 | 56 | /** 57 | * @return Column[] 58 | */ 59 | public function getColumns() { 60 | return $this->columns; 61 | } 62 | 63 | /** 64 | * Provide a representation of this constraint that can be used to calculate a 65 | * fingerprint for whether it has changed or not. 66 | */ 67 | public function __serialize() { 68 | return [ 69 | 'name' => $this->getName(), 70 | 'type' => $this->getType(), 71 | 'columns' => array_map(fn($column) => $column->getName(), $this->getColumns()) 72 | ]; 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/AbstractConstraint.php: -------------------------------------------------------------------------------- 1 | property = $property; 25 | $this->data = isset($validator[$property]) ? $validator[$property] : null; 26 | $this->arguments = $arguments; 27 | 28 | $this->validator = $validator; 29 | } 30 | 31 | /** 32 | * Evaluate the current constraint against the schema arguments and input data. 33 | * 34 | * @return mixed 35 | */ 36 | abstract public function evaluate(); 37 | 38 | 39 | protected function getViolationKey() { 40 | return static::ERROR_INVALID; 41 | } 42 | 43 | /** 44 | * Forms a message that can be displayed in a UI. 45 | * 46 | * @param string $context Allows setting a relevant name for the field 47 | * (for example: 'Your email address' is not valid). 48 | * @return string 49 | */ 50 | protected function getViolationMessage($context = 'This value') { 51 | return sprintf('%s is not valid', $context); 52 | } 53 | 54 | public function getViolationPayload() { 55 | return array( 56 | 'reason' => $this->getViolationKey(), 57 | 'message' => $this->getViolationMessage() 58 | ); 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /src/Wave/DB/Generator/Templates/relations/many-to-many.phpt: -------------------------------------------------------------------------------- 1 | {# value array 9 | * 10 | * @return $this 11 | **/ 12 | public function add{{ relation.Name|singularize }}({{ relation.TargetRelation.ReferencedTable.getClassName(true) }} &$object, $create_relation = true, array $join_data = array()) 13 | { 14 | $this->_addRelationObject('{{ relation.Name }}', $object, $create_relation, $join_data); 15 | return $this; 16 | } 17 | 18 | /** 19 | * {{ relation.Name }} 20 | * 21 | * @param $obj {{ relation.TargetRelation.ReferencedTable.getClassName(true) }} The {{ relation.TargetRelation.ReferencedTable.getClassName() }} object to be removed 22 | * @param bool $remove_relation actually remove the relation from the database 23 | * @return $this 24 | **/ 25 | public function remove{{ relation.Name|singularize }}({{ relation.TargetRelation.ReferencedTable.getClassName(true) }} &$object, $remove_relation = true) 26 | { 27 | $this->_removeRelationObject('{{ relation.Name }}', $object, $remove_relation); 28 | return $this; 29 | } 30 | 31 | /** 32 | * {{ relation.Name }} 33 | * 34 | * @param callable $transform_callback 35 | * @return {{ relation.TargetRelation.ReferencedTable.getClassName(true) }}[] 36 | **/ 37 | public function get{{ relation.Name }}() 38 | { 39 | $transform_callback = func_num_args() >= 1 ? func_get_arg(0) : null; 40 | return $this->_getRelationObjects('{{ relation.Name }}', $transform_callback); 41 | } -------------------------------------------------------------------------------- /src/Wave/Log/CliHandler.php: -------------------------------------------------------------------------------- 1 | '1;34', // light_blue 15 | Logger::INFO => '0;36', // cyan 16 | Logger::WARNING => '1;33', // yellow 17 | Logger::ERROR => '1;31', // light red 18 | Logger::CRITICAL => '0;31', // red 19 | Logger::ALERT => '0;35', // purple 20 | ); 21 | 22 | /** 23 | * Outputs a string to the cli. 24 | * 25 | * @param string|array $record the text to output, or array of lines 26 | */ 27 | protected function write(array $record): void 28 | { 29 | $stream = $record['level'] >= Logger::ERROR ? STDERR : STDOUT; 30 | $text = static::color($record['formatted'], $record['level']); 31 | 32 | $beep = ''; 33 | if($record['level'] >= Logger::CRITICAL) 34 | $beep .= static::beep(); 35 | if($record['level'] >= Logger::ALERT) 36 | $beep .= static::beep(); 37 | 38 | fwrite($stream, $beep . $text . PHP_EOL); 39 | } 40 | 41 | /** 42 | * Beeps a certain number of times. 43 | * 44 | * @param int $num the number of times to beep 45 | */ 46 | public static function beep($num = 1) { 47 | return str_repeat("\x07", $num); 48 | } 49 | 50 | 51 | /** 52 | * Returns the given text with the correct color codes for the given level 53 | * 54 | * @param string $text the text to color 55 | * @param int $level the level of message 56 | * 57 | * @return string the color coded string 58 | */ 59 | public static function color($text, $level) { 60 | 61 | if(isset(static::$colors[$level])) { 62 | $prefix = "\033[" . static::$colors[$level] . "m"; 63 | $text = $prefix . $text . "\033[0m"; 64 | } 65 | 66 | return $text; 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /src/Wave/Http/Response/JsonResponse.php: -------------------------------------------------------------------------------- 1 | query->has('jsonp_callback')) { 31 | $this->jsonp_callback = $request->query->get('jsonp_callback'); 32 | } 33 | 34 | if(!$this->headers->has('Content-Type')) { 35 | $content_types = array_map('trim', explode(',', $request->headers->get('accept', static::$default_type, true))); 36 | $allowed = array_intersect($content_types, static::$acceptable_types); 37 | $content_type = empty($allowed) ? static::$default_type : array_shift($allowed); 38 | 39 | $this->headers->set('Content-Type', $content_type); 40 | } 41 | 42 | $this->headers->set('Cache-Control', 'no-cache, must-revalidate'); 43 | $this->headers->set('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT'); 44 | $this->headers->set('X-Wave-Response', 'json'); 45 | 46 | return $this; 47 | } 48 | 49 | public function setContent($data, $convert = true) { 50 | if($convert) { 51 | $this->data = $data; 52 | $data = JSON::encode($data); 53 | } 54 | 55 | parent::setContent($data); 56 | } 57 | 58 | public function sendContent() { 59 | 60 | if($this->jsonp_callback !== null) { 61 | echo "{$this->jsonp_callback}($this->content);"; 62 | } else parent::sendContent(); 63 | 64 | } 65 | 66 | /** 67 | * @return mixed 68 | */ 69 | public function getData() { 70 | return $this->data; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/ExistsConstraint.php: -------------------------------------------------------------------------------- 1 | type}] constraint requires a model and property to be declared"); 24 | 25 | parent::__construct($property, $arguments, $validator); 26 | 27 | $this->match_fields = $arguments['property']; 28 | if(!is_array($arguments['property'])) { 29 | $this->match_fields = array($arguments['property'] => $property); 30 | } 31 | 32 | if(empty($this->match_fields)) 33 | throw new \InvalidArgumentException("[$this->type] constraint requires at least one property to match"); 34 | 35 | $this->message = isset($arguments['message']) ? $arguments['message'] : null; 36 | } 37 | 38 | /** 39 | * @return bool 40 | */ 41 | public function evaluate() { 42 | $statement = DB::get()->from($this->arguments['model']); 43 | 44 | foreach($this->match_fields as $column => $input_key) { 45 | $statement->where($column . ' = ?', $this->validator[$input_key]); 46 | } 47 | 48 | $this->instance = $statement->fetchRow(); 49 | 50 | return $this->instance instanceof Model; 51 | } 52 | 53 | public function getCleanedData() { 54 | return $this->instance; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | protected function getViolationKey() { 61 | return static::ERROR_NOT_EXISTS; 62 | } 63 | 64 | protected function getViolationMessage($context = 'This value') { 65 | $message = isset($this->message) ? $this->message : '%s does not exist'; 66 | return sprintf($message, $context); 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /src/Wave/Log/ExceptionIntrospectionProcessor.php: -------------------------------------------------------------------------------- 1 | getTrace(); 22 | 23 | $record['extra'] = array_merge( 24 | $record['extra'], 25 | array( 26 | 'file' => $exception->getFile(), 27 | 'line' => $exception->getLine(), 28 | 'class' => isset($trace[0]['class']) ? $trace[0]['class'] : null, 29 | 'function' => isset($trace[0]['function']) ? $trace[0]['function'] : null, 30 | ) 31 | ); 32 | 33 | } else { 34 | // otherwise, reset the end pointer and look for the first non-monolog looking line 35 | reset($trace); 36 | 37 | // skip first since it's always the current method 38 | array_shift($trace); 39 | // the call_user_func call is also skipped 40 | array_shift($trace); 41 | 42 | $i = 0; 43 | while(isset($trace[$i]['class']) 44 | && (false !== strpos($trace[$i]['class'], 'Monolog\\') 45 | || (false !== strpos($trace[$i]['class'], 'Wave\\Log') && 'write' === $trace[$i]['function']))) { 46 | $i++; 47 | } 48 | 49 | // we should have the call source now 50 | $record['extra'] = array_merge( 51 | $record['extra'], 52 | array( 53 | 'file' => isset($trace[$i - 1]['file']) ? $trace[$i - 1]['file'] : null, 54 | 'line' => isset($trace[$i - 1]['line']) ? $trace[$i - 1]['line'] : null, 55 | 'class' => isset($trace[$i]['class']) ? $trace[$i]['class'] : null, 56 | 'function' => isset($trace[$i]['function']) ? $trace[$i]['function'] : null, 57 | ) 58 | ); 59 | } 60 | 61 | return $record; 62 | } 63 | 64 | 65 | } -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/AbstractLengthConstraint.php: -------------------------------------------------------------------------------- 1 | limit = isset($arguments['limit']) 25 | ? $arguments['limit'] 26 | : null; 27 | 28 | $comparators = array( 29 | self::COMPARATOR_ARRAY, 30 | self::COMPARATOR_INT, 31 | self::COMPARATOR_STRING, 32 | self::COMPARATOR_DATETIME 33 | ); 34 | $this->comparator = isset($arguments['comparator']) && in_array($arguments['comparator'], $comparators) 35 | ? $arguments['comparator'] 36 | : null; 37 | 38 | $this->message = isset($arguments['message']) ? $arguments['message'] : null; 39 | } else { 40 | $this->limit = $arguments; 41 | } 42 | 43 | if($this->comparator === null) { 44 | if(is_array($this->data)) 45 | $this->count = self::COMPARATOR_ARRAY; 46 | elseif(is_numeric($this->data)) 47 | $this->comparator = self::COMPARATOR_INT; 48 | elseif(is_string($this->data)) 49 | $this->comparator = self::COMPARATOR_STRING; 50 | elseif($this->data instanceof DateTime) 51 | $this->comparator = self::COMPARATOR_DATETIME; 52 | } 53 | 54 | switch($this->comparator) { 55 | case self::COMPARATOR_ARRAY: 56 | $this->count = count($this->data); 57 | break; 58 | case self::COMPARATOR_STRING: 59 | $this->count = strlen($this->data); 60 | break; 61 | case self::COMPARATOR_DATETIME: 62 | $this->count = $this->data; 63 | break; 64 | default: 65 | $this->count = (double) $this->data; 66 | } 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /src/Wave/View/Tag/Img.php: -------------------------------------------------------------------------------- 1 | getLine(); 18 | 19 | $path = $this->parser->getStream()->expect(Token::STRING_TYPE)->getValue(); 20 | 21 | if(!preg_match('/http(s)?\:\/\//', $path)) 22 | $path = Wave\Config::get('deploy')->assets . $path; 23 | 24 | $attributes = array(); 25 | if($this->parser->getStream()->test(Token::STRING_TYPE)) { 26 | $str = $this->parser->getStream()->expect(Token::STRING_TYPE)->getValue(); 27 | $attributes['title'] = $str; 28 | $attributes['alt'] = $str; 29 | } 30 | if(!$this->parser->getStream()->test(Token::BLOCK_END_TYPE)) { 31 | $array = $this->parser->getExpressionParser()->parseArrayExpression(); 32 | foreach($array->getIterator() as $key => $node) { 33 | $attributes[$key] = $node->getAttribute('value'); 34 | } 35 | } 36 | 37 | $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); 38 | 39 | 40 | return new ImgNode($path, $attributes, $lineno, $this->getTag()); 41 | } 42 | 43 | public function getTag() { 44 | return 'img'; 45 | } 46 | 47 | 48 | } 49 | 50 | class ImgNode extends Node { 51 | 52 | public function __construct($src, $attributes, $line, $tag = null) { 53 | parent::__construct(array(), array('src' => $src, 'attributes' => $attributes), $line, $tag); 54 | } 55 | 56 | public function compile(Compiler $compiler) { 57 | 58 | $src = $this->getAttribute('src'); 59 | $attributes = $this->getAttribute('attributes'); 60 | 61 | if(!isset($attributes['width']) && !isset($attributes['height'])) { 62 | try { 63 | $img = getimagesize($src); 64 | 65 | $attributes['width'] = $img[0]; 66 | $attributes['height'] = $img[1]; 67 | } catch(\Exception $e) { 68 | } 69 | } 70 | 71 | $attributes['src'] = $src; 72 | 73 | $compiled = array(); 74 | foreach($attributes as $key => $value) 75 | $compiled[] = $key . '="' . $value . '"'; 76 | 77 | $compiler 78 | ->addDebugInfo($this) 79 | ->write('echo ') 80 | ->string('') 81 | ->raw(";\n"); 82 | } 83 | 84 | } 85 | 86 | 87 | ?> -------------------------------------------------------------------------------- /src/Wave/DB/Connection.php: -------------------------------------------------------------------------------- 1 | driver); 32 | $this->driver_class = $driver_class; 33 | $this->namespace = $namespace; 34 | 35 | $options = array(); 36 | if(isset($config->driver_options)) { 37 | // driver_options can be in two formats, either a key => value array (native) 38 | // or an array of [ key, value ], used to preserve types (i.e. integers as keys) 39 | if(isset($config->driver_options[0])) { 40 | foreach($config->driver_options as $option) { 41 | list($key, $value) = $option; 42 | $options[$key] = $value; 43 | } 44 | } else 45 | $options = $config->driver_options->getArrayCopy(); 46 | } 47 | 48 | parent::__construct($driver_class::constructDSN($config), $config->username, $config->password, $options); 49 | 50 | $this->cache_enabled = isset($config->enable_cache) && $config->enable_cache; 51 | 52 | //Override the default PDOStatement 53 | $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('\\Wave\\DB\\Statement', array($this))); 54 | 55 | } 56 | 57 | public function prepare($sql, $options = array()): \PDOStatement|false 58 | { 59 | 60 | if(!$this->cache_enabled) 61 | return parent::prepare($sql, $options); 62 | 63 | $hash = md5($sql); 64 | 65 | //double-check that sql is same if it is cached 66 | if(!isset($this->statement_cache[$hash]) || $this->statement_cache[$hash]->queryString !== $sql) { 67 | $this->statement_cache[$hash] = parent::prepare($sql, $options); 68 | } 69 | 70 | return $this->statement_cache[$hash]; 71 | } 72 | 73 | 74 | /** 75 | * @return DriverInterface 76 | */ 77 | public function getDriverClass() { 78 | return $this->driver_class; 79 | } 80 | 81 | /** 82 | * Return the namespace this connection 83 | * was established within 84 | * 85 | * @return mixed 86 | */ 87 | public function getNamespace() { 88 | return $this->namespace; 89 | } 90 | 91 | 92 | } 93 | 94 | ?> -------------------------------------------------------------------------------- /src/Wave/View/Tag/Register.php: -------------------------------------------------------------------------------- 1 | getLine(); 19 | 20 | $extras = ''; 21 | 22 | $type = $this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(); 23 | 24 | if(!in_array($type, array(self::TYPE_JS, self::TYPE_CSS))) 25 | throw new SyntaxError("Register type must be 'css' or 'js'.", $lineno, $token->getFilename()); 26 | 27 | $file = $this->parser->getStream()->expect(Token::STRING_TYPE)->getValue(); 28 | 29 | if($type == self::TYPE_CSS) { 30 | if($this->parser->getStream()->test(Token::STRING_TYPE)) 31 | $extras = $this->parser->getStream()->expect(Token::STRING_TYPE)->getValue(); 32 | else 33 | $extras = 'all'; 34 | } 35 | 36 | if($this->parser->getStream()->test(Token::NUMBER_TYPE)) 37 | $priority = $this->parser->getStream()->expect(Token::NUMBER_TYPE)->getValue(); 38 | else 39 | $priority = 0; 40 | 41 | $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); 42 | 43 | return new RegisterNode($type, $file, $extras, $priority, $lineno, $this->getTag()); 44 | } 45 | 46 | public function getTag() { 47 | return 'register'; 48 | } 49 | 50 | 51 | } 52 | 53 | class RegisterNode extends Node { 54 | 55 | public function __construct($type, $file, $extras, $priority, $line, $tag = null) { 56 | parent::__construct( 57 | array(), array( 58 | 'type' => $type, 59 | 'file' => $file, 60 | 'extras' => $extras, 61 | 'priority' => $priority 62 | ), $line, $tag 63 | ); 64 | } 65 | 66 | public function compile(Compiler $compiler) { 67 | 68 | $type = $this->getAttribute('type'); 69 | $file = $this->getAttribute('file'); 70 | $extras = $this->getAttribute('extras'); 71 | $priority = $this->getAttribute('priority'); 72 | $cache_tag = null; 73 | if(!preg_match('/http(s)?\:\/\//', $file)) { 74 | $file = Wave\Config::get('deploy')->assets . $file; 75 | $cache_tag = @file_get_contents($file); 76 | if($cache_tag !== false) $cache_tag = md5($cache_tag); 77 | else $cache_tag = null; 78 | } 79 | 80 | $compiler 81 | ->addDebugInfo($this) 82 | ->write("\$this->env->_wave_register('{$type}', '{$file}', '{$extras}', '{$priority}', '{$cache_tag}');") 83 | ->raw("\n"); 84 | } 85 | 86 | } 87 | 88 | 89 | ?> -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/TypeConstraint.php: -------------------------------------------------------------------------------- 1 | message = '%s is not a valid %s'; 27 | if(is_array($arguments) && !is_callable($arguments)) { 28 | if(isset($arguments['message'], $arguments['datatype'])) { 29 | $this->message = $arguments['message']; 30 | $this->arguments = $arguments = $arguments['datatype']; 31 | } else throw new \InvalidArgumentException("Invalid format for type constraint, must contain a [message] and [datatype]"); 32 | } 33 | 34 | if(is_callable($arguments)) { 35 | $this->handler = $arguments; 36 | } else if(is_string($arguments)) { 37 | $handler_class = sprintf(self::DATATYPE_CLASS_MASK, ucfirst($arguments)); 38 | if(!class_exists($handler_class)) 39 | throw new \InvalidArgumentException("'type' handler '$arguments' is not valid for '$property'"); 40 | 41 | $this->handler = new $handler_class($this->data); 42 | } else { 43 | throw new \InvalidArgumentException("Invalid 'type' specified for $property"); 44 | } 45 | } 46 | 47 | /** 48 | * @return bool 49 | */ 50 | public function evaluate() { 51 | return call_user_func($this->handler, $this->data, $this->validator); 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | protected function getViolationKey() { 58 | return static::ERROR_INVALID; 59 | } 60 | 61 | protected function getViolationMessage($context = 'This value') { 62 | $type = 'type'; 63 | if($this->handler instanceof AbstractDatatype) 64 | $type = $this->handler->getType(); 65 | 66 | return sprintf($this->message, $context, $type); 67 | } 68 | 69 | public function getViolationPayload() { 70 | return array_merge( 71 | parent::getViolationPayload(), 72 | array( 73 | 'type' => is_callable($this->arguments) ? 'custom' : $this->arguments 74 | ) 75 | ); 76 | } 77 | 78 | public function getCleanedData() { 79 | if($this->handler instanceof CleanerInterface) 80 | return $this->handler->getCleanedData(); 81 | else 82 | return $this->data; 83 | } 84 | } -------------------------------------------------------------------------------- /src/Wave/Router/Generator.php: -------------------------------------------------------------------------------- 1 | path->controllers); 13 | $reflected_options = $reflector->execute(); 14 | 15 | $all_actions = self::buildRoutes($reflected_options); 16 | foreach($all_actions as $profile => $actions) { 17 | $route_node = new Node(); 18 | foreach($actions as $action) { 19 | foreach($action->getRoutes() as $route) { 20 | $route_node->addChild($route, $action); 21 | } 22 | } 23 | Wave\Cache::store(Router::getCacheName($profile, 'table'), $actions); 24 | Wave\Cache::store(Router::getCacheName($profile, 'tree'), $route_node); 25 | } 26 | } 27 | 28 | public static function buildRoutes($controllers) { 29 | 30 | $compiled_routes = array(); 31 | // iterate all the controllers and make a tree of all the possible path 32 | foreach($controllers as $controller) { 33 | $base_route = new Action(); 34 | // set the route defaults from the Controller annotations (if any) 35 | foreach($controller['class']['annotations'] as $annotation) { 36 | $base_route->addAnnotation($annotation); 37 | } 38 | 39 | foreach($controller['methods'] as $method) { 40 | $route = clone $base_route; // copy from the controller route 41 | 42 | if($method['visibility'] == Wave\Reflector::VISIBILITY_PUBLIC) { 43 | foreach($method['annotations'] as $annotation){ 44 | $route->addAnnotation($annotation); 45 | } 46 | 47 | foreach($method['parameters'] as $parameter) { 48 | /** @var \ReflectionParameter $parameter */ 49 | $type = null; 50 | $reflected_type = $parameter->getType(); 51 | if ($reflected_type instanceof ReflectionType) { 52 | $type = $reflected_type->getName(); 53 | } 54 | 55 | $route->addMethodParameter($parameter->getName(), $type); 56 | } 57 | 58 | } 59 | 60 | $route->setAction($controller['class']['name'] . '.' . $method['name']); 61 | 62 | if($route->hasRoutes() || $route->hasSchedule()) { 63 | if(isset($compiled_routes[$base_route->getProfile()][$route->getAction()])) { 64 | throw new \LogicException(sprintf("Action %s is declared twice", $route->getAction())); 65 | } 66 | $compiled_routes[$base_route->getProfile()][$route->getAction()] = $route; 67 | } 68 | } 69 | } 70 | 71 | return $compiled_routes; 72 | } 73 | 74 | } 75 | 76 | 77 | ?> -------------------------------------------------------------------------------- /src/Wave/Http/Cookie.php: -------------------------------------------------------------------------------- 1 | name = $name; 25 | $this->value = $value; 26 | $this->domain = $domain; 27 | $this->expire = $expire; 28 | $this->path = empty($path) ? '/' : $path; 29 | $this->secure = !!$secure; 30 | $this->httpOnly = !!$httpOnly; 31 | } 32 | 33 | /** 34 | * Gets the name of the cookie. 35 | * 36 | * @return string 37 | * 38 | * @api 39 | */ 40 | public function getName() { 41 | return $this->name; 42 | } 43 | 44 | /** 45 | * Gets the value of the cookie. 46 | * 47 | * @return string 48 | * 49 | * @api 50 | */ 51 | public function getValue() { 52 | return $this->value; 53 | } 54 | 55 | /** 56 | * Gets the domain that the cookie is available to. 57 | * 58 | * @return string 59 | * 60 | * @api 61 | */ 62 | public function getDomain() { 63 | return $this->domain; 64 | } 65 | 66 | /** 67 | * Gets the time the cookie expires. 68 | * 69 | * @return integer 70 | * 71 | * @api 72 | */ 73 | public function getExpires() { 74 | return $this->expire; 75 | } 76 | 77 | /** 78 | * Gets the path on the server in which the cookie will be available on. 79 | * 80 | * @return string 81 | * 82 | * @api 83 | */ 84 | public function getPath() { 85 | return $this->path; 86 | } 87 | 88 | /** 89 | * Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client. 90 | * 91 | * @return Boolean 92 | * 93 | * @api 94 | */ 95 | public function isSecure() { 96 | return $this->secure; 97 | } 98 | 99 | /** 100 | * Checks whether the cookie will be made accessible only through the HTTP protocol. 101 | * 102 | * @return Boolean 103 | * 104 | * @api 105 | */ 106 | public function isHttpOnly() { 107 | return $this->httpOnly; 108 | } 109 | 110 | /** 111 | * Whether this cookie is about to be cleared 112 | * 113 | * @return Boolean 114 | * 115 | * @api 116 | */ 117 | public function isCleared() { 118 | return $this->expire < time(); 119 | } 120 | } -------------------------------------------------------------------------------- /bin/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | basicQuery('SELECT 1 as result'); 52 | if(isset($result[0], $result[0]['result']) && $result[0]['result'] === '1') 53 | return info('Connected to database successfully'); 54 | else { 55 | error('Connected to the database, but the result was not expected', false); 56 | return error(print_r($result, true)); 57 | } 58 | } catch(Exception $e) { 59 | error("Could not connect to database.", false); 60 | error($e->getMessage()); 61 | } 62 | } 63 | 64 | 65 | function read($prompt = null, $delim = ':') { 66 | if($prompt !== null) { 67 | echo "{$prompt}{$delim}\n"; 68 | } 69 | if(function_exists('readline')) return readline(); 70 | else { 71 | $fr = fopen("php://stdin", "r"); 72 | $input = fgets($fr, 128); 73 | $input = rtrim($input); 74 | fclose($fr); 75 | return $input; 76 | } 77 | } 78 | 79 | function info($message) { 80 | echo "\033[1;37m$message\033[0m\n"; 81 | } 82 | 83 | function warn($message) { 84 | echo "\033[1;34m$message\033[0m\n"; 85 | } 86 | 87 | function error($message, $terminiate = true) { 88 | echo "\033[0;31m$message\033[0m\n"; 89 | if($terminiate) exit(1); 90 | } -------------------------------------------------------------------------------- /src/Wave/Validator/Constraints/AnyConstraint.php: -------------------------------------------------------------------------------- 1 | false 17 | ); 18 | 19 | private $cleaned = null; 20 | private $violations = array(); 21 | 22 | private $message = null; 23 | 24 | public function __construct($property, $arguments, Validator &$validator) { 25 | parent::__construct($property, $arguments, $validator); 26 | 27 | // inherit the default value from the parent instance of 28 | $schema = $validator->getSchemaKey($property); 29 | if(array_key_exists('default', $schema)) { 30 | $this->inherit_schema['default'] = $schema['default']; 31 | } 32 | } 33 | 34 | 35 | /** 36 | * @throws \InvalidArgumentException 37 | * @return bool 38 | */ 39 | public function evaluate() { 40 | 41 | if(!is_array($this->arguments)) 42 | throw new \InvalidArgumentException("[any] constraint requires an array argument"); 43 | if(!isset($this->arguments[0])) 44 | $this->arguments = array($this->arguments); 45 | 46 | $input = array($this->property => $this->data); 47 | foreach($this->arguments as $key => $constraint_group) { 48 | 49 | if($key === 'message') { 50 | $this->message = $constraint_group; 51 | continue; 52 | } 53 | 54 | $instance = new Validator($input, array( 55 | 'fields' => array( 56 | $this->property => array_replace($this->inherit_schema, $constraint_group) 57 | ) 58 | ), $this->validator); 59 | 60 | if($instance->execute()) { 61 | $cleaned = $instance->getCleanedData(); 62 | if(isset($cleaned[$this->property])) { 63 | $this->cleaned = $cleaned[$this->property]; 64 | } 65 | return true; 66 | } else { 67 | $violations = $instance->getViolations(); 68 | $messages = array_intersect_key($violations[$this->property], array_flip(array('reason', 'message'))); 69 | if(!empty($messages)) 70 | $this->violations[] = $messages; 71 | } 72 | } 73 | return empty($this->violations); 74 | } 75 | 76 | public function getViolationPayload() { 77 | $payload = array( 78 | 'reason' => 'invalid', 79 | ); 80 | if($this->message !== null) { 81 | $payload['message'] = $this->message; 82 | } else { 83 | $payload['message'] = 'This value does not match any of the following conditions'; 84 | $payload['conditions'] = $this->violations; 85 | } 86 | return $payload; 87 | } 88 | 89 | public function getCleanedData() { 90 | return $this->cleaned; 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /src/Wave/Annotation.php: -------------------------------------------------------------------------------- 1 | BaseRoute::class, 28 | 'baseurl' => BaseURL::class, 29 | 'requireslevel' => RequiresLevel::class, 30 | 'respondswith' => RespondsWith::class, 31 | 'route' => Route::class, 32 | 'validate' => Validate::class, 33 | 'schedule' => Schedule::class, 34 | ); 35 | 36 | public static function factory($key, $value, $from_class = null){ 37 | 38 | $class = __CLASS__; 39 | if(isset(self::$handlers[$key])){ 40 | $class = self::$handlers[$key]; 41 | } 42 | 43 | return new $class($key, $value, $from_class); 44 | } 45 | 46 | public static function parse($block, $originating_class){ 47 | 48 | if(empty($block)) return array(); 49 | 50 | $block = self::sanitizeDocBlock($block); 51 | 52 | $annotations = array(); 53 | $pattern = '/^[~@](?\w+)(?:[\t\f ](?.+?(?=\s[~@\s])))?/ms'; 54 | preg_match_all($pattern, $block, $found); 55 | 56 | foreach ($found['annotation'] as $position => $annotation) { 57 | $arguments = $found['arguments'][$position]; 58 | $type = strtolower($annotation); 59 | $annotations[] = self::factory($type, $arguments, $originating_class); 60 | } 61 | return $annotations; 62 | 63 | } 64 | 65 | /** 66 | * Remove comment notation (/ and *) from a raw docblock 67 | * 68 | * @param $docblock 69 | * @return mixed 70 | */ 71 | protected static function sanitizeDocBlock($docblock){ 72 | return preg_replace('/^([\t\f ]*\*[\t\f ]+)/m', '', $docblock); 73 | } 74 | 75 | public function __construct($key, $value, $from_class = null) { 76 | $this->key = $key; 77 | $this->value = $value; 78 | $this->from_class = $from_class; 79 | } 80 | 81 | public function apply(Router\Action &$action){} 82 | 83 | /** 84 | * @return mixed 85 | */ 86 | public function getKey() { 87 | return $this->key; 88 | } 89 | 90 | /** 91 | * @return mixed 92 | */ 93 | public function getValue() { 94 | return $this->value; 95 | } 96 | 97 | protected function validOnSubclassesOf($annotatedClass, $baseClass) { 98 | if( $annotatedClass != $baseClass && !is_subclass_of($annotatedClass, $baseClass) ) 99 | throw new InvalidAnnotationException(get_class($this) . " is only valid on objects of type {$baseClass}."); 100 | 101 | } 102 | 103 | 104 | } -------------------------------------------------------------------------------- /src/Wave/Http/ServerBag.php: -------------------------------------------------------------------------------- 1 | 12 | * @author Bulat Shakirzyanov 13 | * @author Robert Kiss 14 | * 15 | */ 16 | 17 | namespace Wave\Http; 18 | 19 | /** 20 | * ServerBag is a container for HTTP headers from the $_SERVER variable. 21 | */ 22 | class ServerBag extends ParameterBag { 23 | 24 | /** 25 | * Gets the HTTP headers. 26 | * 27 | * @return array 28 | */ 29 | public function getHeaders() { 30 | $headers = array(); 31 | foreach($this->parameters as $key => $value) { 32 | if(0 === strpos($key, 'HTTP_')) { 33 | $headers[substr($key, 5)] = $value; 34 | } // CONTENT_* are not prefixed with HTTP_ 35 | elseif(in_array($key, array('CONTENT_LENGTH', 'CONTENT_MD5', 'CONTENT_TYPE'))) { 36 | $headers[$key] = $value; 37 | } 38 | } 39 | 40 | if(isset($this->parameters['PHP_AUTH_USER'])) { 41 | $headers['PHP_AUTH_USER'] = $this->parameters['PHP_AUTH_USER']; 42 | $headers['PHP_AUTH_PW'] = isset($this->parameters['PHP_AUTH_PW']) ? $this->parameters['PHP_AUTH_PW'] : ''; 43 | } else { 44 | /* 45 | * php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default 46 | * For this workaround to work, add these lines to your .htaccess file: 47 | * RewriteCond %{HTTP:Authorization} ^(.+)$ 48 | * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 49 | * 50 | * A sample .htaccess file: 51 | * RewriteEngine On 52 | * RewriteCond %{HTTP:Authorization} ^(.+)$ 53 | * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 54 | * RewriteCond %{REQUEST_FILENAME} !-f 55 | * RewriteRule ^(.*)$ app.php [QSA,L] 56 | */ 57 | 58 | $authorizationHeader = null; 59 | if(isset($this->parameters['HTTP_AUTHORIZATION'])) { 60 | $authorizationHeader = $this->parameters['HTTP_AUTHORIZATION']; 61 | } elseif(isset($this->parameters['REDIRECT_HTTP_AUTHORIZATION'])) { 62 | $authorizationHeader = $this->parameters['REDIRECT_HTTP_AUTHORIZATION']; 63 | } 64 | 65 | // Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic 66 | if((null !== $authorizationHeader) && (0 === stripos($authorizationHeader, 'basic'))) { 67 | $exploded = explode(':', base64_decode(substr($authorizationHeader, 6))); 68 | if(count($exploded) == 2) { 69 | list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $exploded; 70 | } 71 | } 72 | } 73 | 74 | // PHP_AUTH_USER/PHP_AUTH_PW 75 | if(isset($headers['PHP_AUTH_USER'])) { 76 | $headers['AUTHORIZATION'] = 'Basic ' . base64_encode($headers['PHP_AUTH_USER'] . ':' . $headers['PHP_AUTH_PW']); 77 | } 78 | 79 | return $headers; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Wave/Annotation/ArrayArguments.php: -------------------------------------------------------------------------------- 1 | parameters = $this->parseParameters(); 18 | 19 | $this->validate($from_class); 20 | if(!empty($this->errors)){ 21 | throw new InvalidAnnotationException('Annotation format error, '.implode(', ', $this->errors), 0); 22 | } 23 | 24 | $this->build(); 25 | } 26 | 27 | protected function validate($class){} 28 | protected function build(){} 29 | 30 | protected function parseParameters(){ 31 | 32 | $arguments = array(); 33 | foreach(explode(',', $this->value) as $argument){ 34 | // attempt to explode the argument into a key:value 35 | list($k, $value) = explode(':', $argument) + array(null, null); 36 | // no value means it was just a plain argument, so just clean it and insert as normal 37 | $k = trim($k, ' \'"'); 38 | if($value === null){ 39 | $arguments[] = $k; 40 | } 41 | else { 42 | $arguments[strtolower($k)] = trim($value, ' \'"'); 43 | } 44 | } 45 | 46 | return $arguments; 47 | 48 | } 49 | 50 | protected function acceptedKeys($keys) { 51 | foreach($this->parameters as $key => $value) { 52 | if (is_string($key) && !in_array($key, $keys)) { 53 | $this->errors[] = "Invalid parameter: \"$key\"."; 54 | } 55 | } 56 | } 57 | 58 | protected function requiredKeys($keys) { 59 | foreach($keys as $key) { 60 | if(!array_key_exists($key, $this->parameters)) { 61 | $this->errors[] = get_class($this) . " requires a '$key' parameter."; 62 | } 63 | } 64 | } 65 | 66 | protected function acceptedKeylessValues($values) { 67 | foreach($this->parameters as $key => $value) { 68 | if(!is_string($key) && !in_array($value, $values)) { 69 | $this->errors[] = "Unknown parameter: \"$value\"."; 70 | } 71 | } 72 | } 73 | 74 | protected function acceptedIndexedValues($index, $values, $optional = true) { 75 | if($optional && !isset($this->parameters[$index])) return; 76 | 77 | if(!in_array($this->parameters[$index],$values)) { 78 | $this->errors[] = "Parameter $index is set to \"" . $this->parameters[$index] . "\". Valid values: " . implode(', ', $values) . '.'; 79 | } 80 | } 81 | 82 | protected function acceptsNoKeylessValues() { 83 | $this->acceptedKeylessValues(array()); 84 | } 85 | 86 | protected function acceptsNoKeyedValues() { 87 | $this->acceptedKeys(array()); 88 | } 89 | 90 | 91 | protected function minimumParameterCount($count) { 92 | if( ! (count($this->parameters) >= $count) ) { 93 | $this->errors[] = get_class($this) . " takes at least $count parameters."; 94 | } 95 | } 96 | 97 | protected function maximumParameterCount($count) { 98 | if( ! (count($this->parameters) <= $count) ) { 99 | $this->errors[] = get_class($this) . " takes at most $count parameters."; 100 | } 101 | } 102 | 103 | protected function exactParameterCount($count) { 104 | if ( count($this->parameters) != $count ) { 105 | $this->errors[] = get_class($this) . " requires exactly $count parameters."; 106 | } 107 | } 108 | 109 | 110 | } -------------------------------------------------------------------------------- /src/Wave/Auth.php: -------------------------------------------------------------------------------- 1 | $value) { 46 | if((is_callable($value) && $value($auth_object->$key)) 47 | || (isset($auth_object->$key) && $auth_object->$key == $value) 48 | ) 49 | continue; 50 | 51 | self::$_auth_problems['secondary'][$key] = array( 52 | 'value' => $auth_object->$key, 53 | 'reason' => self::FAILURE_BAD_CREDENTIAL, 54 | 'match' => $value 55 | ); 56 | $_is_valid = false; 57 | } 58 | 59 | if($_is_valid) { 60 | self::$_valid_auth = $auth_object; 61 | self::registerIdentity($auth_object); 62 | return self::SUCCESS; 63 | } else return self::FAILURE_BAD_CREDENTIAL; 64 | 65 | } else { 66 | self::$_auth_problems['primary'] = $primary; 67 | 68 | return self::FAILURE_NO_IDENTITY; 69 | } 70 | } 71 | 72 | public static function registerIdentity($identity) { 73 | return Registry::store('__wave_identity', $identity); 74 | } 75 | 76 | public static function deregisterIdentity() { 77 | return Registry::destroy('__wave_identity'); 78 | } 79 | 80 | public static function persistIdentity($identity, $type = null, $expires = null) { 81 | $config = Config::get('deploy')->auth; 82 | if($type === null) 83 | $type = $config->persist_type; 84 | if($expires === null) 85 | $expires = $config->$type->expires; 86 | 87 | if($type == 'cookie') { 88 | 89 | Cookie::store( 90 | $config->cookie->name, 91 | $identity, 92 | strtotime($expires), 93 | $config->cookie->path, 94 | $config->cookie->domain, 95 | isset($config->cookie->secure) ? $config->cookie->secure : false, 96 | isset($config->cookie->httponly) ? $config->cookie->httponly : true 97 | ); 98 | } 99 | 100 | 101 | } 102 | 103 | public static function ceaseIdentity($type = null) { 104 | $config = Config::get('deploy')->auth; 105 | if($type === null) 106 | $type = $config->persist_type; 107 | 108 | if($type == 'cookie') { 109 | 110 | Cookie::store( 111 | $config->cookie->name, 112 | '', 113 | time() - 86400, 114 | $config->cookie->path, 115 | $config->cookie->domain 116 | ); 117 | } 118 | 119 | 120 | } 121 | 122 | public static function getAuthProblems() { 123 | return self::$_auth_problems; 124 | } 125 | 126 | public static function getIdentity() { 127 | return Registry::fetch('__wave_identity'); 128 | } 129 | 130 | public static function getHandlerClass() { 131 | return self::$_handler; 132 | } 133 | 134 | } 135 | 136 | 137 | ?> -------------------------------------------------------------------------------- /src/Wave/Router/Node.php: -------------------------------------------------------------------------------- 1 | '; 12 | const VAR_STRING = ''; 13 | 14 | const URL_SEGMENT_DELIMITER = '/(?)/'; 15 | 16 | private $children = array(); 17 | private $action = null; 18 | 19 | private function setAction(Action $action) { 20 | if($this->action === null) 21 | $this->action = $action; 22 | else { 23 | throw new Exception($this->action->getAction() . ' shares a duplicate route with ' . $action->getAction()); 24 | } 25 | } 26 | 27 | public function addChild($route, Action $action) { 28 | 29 | // need to check if this part of the segment is a regex 30 | // and extend the segment to contain the whole expression 31 | // if it contains a `/` 32 | if(substr_compare($route, '[^\/]*/', $route, $matches); 34 | $segment = $matches[0]; 35 | $segment_length = strlen($segment); 36 | if($segment_length < strlen($route)) 37 | $remaining = substr($route, $segment_length + 1); 38 | } else { 39 | list($segment, $remaining) = preg_split(self::URL_SEGMENT_DELIMITER, $route, 2) + array(null, null); 40 | } 41 | 42 | if(!isset($this->children[$segment])) { 43 | $this->children[$segment] = new Node(); 44 | } 45 | 46 | if(isset($remaining) && strlen($remaining) > 0) { 47 | $this->children[$segment]->addChild($remaining, $action); 48 | } else 49 | $this->children[$segment]->setAction($action); 50 | 51 | } 52 | 53 | public function findChild($url, Request &$request) { 54 | 55 | if($url == null) return $this; 56 | 57 | $segment = preg_split(self::URL_SEGMENT_DELIMITER, $url, 2); 58 | if(isset($segment[1])) $remaining = $segment[1]; 59 | else $remaining = null; 60 | $segment = $segment[0]; 61 | 62 | $node = null; 63 | // first check the segment is a directly keyed child 64 | if(isset($this->children[$segment])) { 65 | $node = $this->children[$segment]; 66 | // if there is more to go, recurse with the rest 67 | return $node !== null ? $node->findChild($remaining, $request) : $node; 68 | } else { 69 | // otherwise, start searching through all the child paths 70 | // matching each one and recursing if a match is found 71 | $matching_node = null; 72 | foreach($this->children as $path => $node) { 73 | // start with the regex matches 74 | if(substr_compare($path, ''); 76 | $pattern = '#^' . substr($path, 2, $expression_end - 2) . '#'; 77 | if(preg_match($pattern, $url, $matches) == 1) { 78 | $segment = $matches[0]; 79 | $remaining = substr($url, strlen($segment) + 1) ?: null; 80 | $matching_node = $node->findChild($remaining, $request); 81 | if($matching_node !== null && $matching_node->hasValidAction()) 82 | $request->attributes->set(substr($path, $expression_end + 2), $segment); 83 | } 84 | } elseif((is_numeric($segment) && strpos($path, self::VAR_INT) !== false) 85 | || strpos($path, self::VAR_STRING) !== false 86 | ) { 87 | 88 | $matching_node = $node->findChild($remaining, $request); 89 | if($matching_node !== null && $matching_node->hasValidAction()) 90 | $request->attributes->set(substr($path, strpos($path, '>') + 1), $segment); 91 | } 92 | 93 | if($matching_node !== null) 94 | break; 95 | } 96 | 97 | return $matching_node; 98 | } 99 | } 100 | 101 | public function getAction() { 102 | return $this->action; 103 | } 104 | 105 | public function hasValidAction() { 106 | return $this->action instanceof Action; 107 | } 108 | 109 | } 110 | 111 | ?> -------------------------------------------------------------------------------- /src/Wave/DB/Table.php: -------------------------------------------------------------------------------- 1 | database = $database; 37 | $this->table = $table; 38 | $this->engine = $engine; 39 | $this->collation = $collation; 40 | $this->comment = $comment; 41 | } 42 | 43 | /** 44 | * @return Column[] 45 | */ 46 | public function getColumns() { 47 | 48 | if(!isset($this->columns)) { 49 | $driver_class = $this->database->getConnection()->getDriverClass(); 50 | $this->columns = $driver_class::getColumns($this); 51 | } 52 | 53 | return $this->columns; 54 | } 55 | 56 | /** 57 | * @return Relation[] 58 | */ 59 | public function getRelations() { 60 | 61 | if(!isset($this->relations)) { 62 | $driver_class = $this->database->getConnection()->getDriverClass(); 63 | $this->relations = $driver_class::getRelations($this); 64 | } 65 | 66 | return $this->relations; 67 | } 68 | 69 | /** 70 | * @return Constraint[] 71 | */ 72 | public function getConstraints() { 73 | 74 | if(!isset($this->constraints)) { 75 | $driver_class = $this->database->getConnection()->getDriverClass(); 76 | $this->constraints = $driver_class::getConstraints($this); 77 | } 78 | 79 | return $this->constraints; 80 | 81 | } 82 | 83 | /** 84 | * @return null|Constraint 85 | */ 86 | public function getPrimaryKey() { 87 | 88 | foreach($this->getConstraints() as $constraint) 89 | if($constraint->getType() === Constraint::TYPE_PRIMARY) 90 | return $constraint; 91 | 92 | return null; 93 | } 94 | 95 | /** 96 | * @return \Wave\DB 97 | */ 98 | public function getDatabase() { 99 | return $this->database; 100 | } 101 | 102 | /** 103 | * @return string 104 | */ 105 | public function getName() { 106 | return $this->table; 107 | } 108 | 109 | /** 110 | * @return string 111 | */ 112 | public function getEngine() { 113 | return $this->engine; 114 | } 115 | 116 | /** 117 | * @return string 118 | */ 119 | public function getCollation() { 120 | return $this->collation; 121 | } 122 | 123 | /** 124 | * @return string 125 | */ 126 | public function getComment() { 127 | return $this->comment; 128 | } 129 | 130 | /** 131 | * @param bool $with_namespace 132 | * 133 | * @return string 134 | */ 135 | public function getClassName($with_namespace = false) { 136 | 137 | $prefix = $with_namespace ? '\\' . $this->database->getNamespace() . '\\' : ''; 138 | return $prefix . Wave\Inflector::camelize($this->table); 139 | } 140 | 141 | /** 142 | * Returns a fingerprint of this table schema that can be used to calculate if 143 | * the table defintion has changed. This is primarily used when generating models 144 | * to avoid recreating models that haven't changed. 145 | * 146 | * Makes use of overloaded __serialize() methods on the Column, Relation, and 147 | * Constraint components to calculate a MD5 hash. 148 | */ 149 | public function getSchemaFingerprint() { 150 | $fingerprint = [ 151 | 'database' => $this->database->getName(), 152 | 'table' => $this->table, 153 | 'engine' => $this->engine, 154 | 'collation' => $this->collation, 155 | 'comment' => $this->comment, 156 | 'columns' => $this->getColumns(), 157 | 'relations' => $this->getRelations(), 158 | 'constraints' => $this->getConstraints() 159 | ]; 160 | return md5(serialize($fingerprint)); 161 | } 162 | 163 | } -------------------------------------------------------------------------------- /src/Wave/Inflector.php: -------------------------------------------------------------------------------- 1 | "$1zes", 9 | '/^(ox)$/i' => "$1en", 10 | '/([m|l])ouse$/i' => "$1ice", 11 | '/(matr|vert|ind)ix|ex$/i' => "$1ices", 12 | '/(x|ch|ss|sh)$/i' => "$1es", 13 | '/([^aeiouy]|qu)y$/i' => "$1ies", 14 | '/(hive)$/i' => "$1s", 15 | '/(?:([^f])fe|([lr])f)$/i' => "$1$2ves", 16 | '/(shea|lea|loa|thie)f$/i' => "$1ves", 17 | '/sis$/i' => "ses", 18 | '/([ti])um$/i' => "$1a", 19 | '/(tomat|potat|ech|her|vet)o$/i' => "$1oes", 20 | '/(bu)s$/i' => "$1ses", 21 | '/(alias)$/i' => "$1es", 22 | '/(octop)us$/i' => "$1i", 23 | '/(ax|test)is$/i' => "$1es", 24 | '/(us)$/i' => "$1es", 25 | '/s$/i' => "s", 26 | '/$/' => "s" 27 | ); 28 | 29 | private static $singular = array( 30 | '/(quiz)zes$/i' => "$1", 31 | '/(matr)ices$/i' => "$1ix", 32 | '/(vert|ind)ices$/i' => "$1ex", 33 | '/^(ox)en$/i' => "$1", 34 | '/(alias)es$/i' => "$1", 35 | '/(octop|vir)i$/i' => "$1us", 36 | '/(cris|ax|test)es$/i' => "$1is", 37 | '/(shoe)s$/i' => "$1", 38 | '/(o)es$/i' => "$1", 39 | '/(bus)es$/i' => "$1", 40 | '/([m|l])ice$/i' => "$1ouse", 41 | '/(x|ch|ss|sh)es$/i' => "$1", 42 | '/(m)ovies$/i' => "$1ovie", 43 | '/(s)eries$/i' => "$1eries", 44 | '/([^aeiouy]|qu)ies$/i' => "$1y", 45 | '/([lr])ves$/i' => "$1f", 46 | '/(tive)s$/i' => "$1", 47 | '/(hive)s$/i' => "$1", 48 | '/(li|wi|kni)ves$/i' => "$1fe", 49 | '/(shea|loa|lea|thie)ves$/i' => "$1f", 50 | '/(^analy)ses$/i' => "$1sis", 51 | '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => "$1$2sis", 52 | '/([ti])a$/i' => "$1um", 53 | '/(n)ews$/i' => "$1ews", 54 | '/(h|bl)ouses$/i' => "$1ouse", 55 | '/(corpse)s$/i' => "$1", 56 | '/(us)es$/i' => "$1", 57 | '/([^us])s$/i' => "$1" 58 | ); 59 | 60 | private static $irregular = array( 61 | 'move' => 'moves', 62 | 'foot' => 'feet', 63 | 'goose' => 'geese', 64 | 'sex' => 'sexes', 65 | 'child' => 'children', 66 | 'man' => 'men', 67 | 'tooth' => 'teeth', 68 | 'person' => 'people' 69 | ); 70 | 71 | private static $uncountable = array( 72 | 'sheep', 73 | 'fish', 74 | 'deer', 75 | 'series', 76 | 'species', 77 | 'money', 78 | 'rice', 79 | 'information', 80 | 'equipment', 81 | 'meta', 82 | 'media' 83 | ); 84 | 85 | public static function pluralize($string) { 86 | // save some time in the case that singular and plural are the same 87 | if(in_array(strtolower($string), self::$uncountable)) 88 | return $string; 89 | 90 | // check for irregular singular forms 91 | foreach(self::$irregular as $pattern => $result) { 92 | $pattern = '/' . $pattern . '$/i'; 93 | 94 | if(preg_match($pattern, $string)) 95 | return preg_replace($pattern, $result, $string); 96 | } 97 | 98 | // check for matches using regular expressions 99 | foreach(self::$plural as $pattern => $result) { 100 | if(preg_match($pattern, $string)) 101 | return preg_replace($pattern, $result, $string); 102 | } 103 | 104 | return $string; 105 | } 106 | 107 | public static function singularize($string) { 108 | // save some time in the case that singular and plural are the same 109 | if(in_array(strtolower($string), self::$uncountable)) 110 | return $string; 111 | 112 | // check for irregular plural forms 113 | foreach(self::$irregular as $result => $pattern) { 114 | $pattern = '/' . $pattern . '$/i'; 115 | 116 | if(preg_match($pattern, $string)) 117 | return preg_replace($pattern, $result, $string); 118 | } 119 | 120 | // check for matches using regular expressions 121 | foreach(self::$singular as $pattern => $result) { 122 | if(preg_match($pattern, $string)) 123 | return preg_replace($pattern, $result, $string); 124 | } 125 | 126 | return $string; 127 | } 128 | 129 | public static function camelize($string, $suffix_to_remove = '') { 130 | 131 | $camelized = ''; 132 | $words = preg_split('/([^a-zA-Z])/', rtrim($string, $suffix_to_remove), -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); 133 | 134 | foreach($words as $word) 135 | if(!in_array($word, array('_', ' '))) 136 | $camelized .= ucfirst($word); 137 | 138 | return $camelized; 139 | } 140 | 141 | } 142 | 143 | ?> -------------------------------------------------------------------------------- /src/Wave/Utils/XML.php: -------------------------------------------------------------------------------- 1 | openMemory(); 18 | $xml->startDocument('1.0', 'UTF-8'); 19 | $xml->startElement($rootNodeName); 20 | 21 | $data = self::arrayify($data); 22 | function write(XMLWriter $xml, $data) { 23 | foreach($data as $_key => $value) { 24 | // check the key isnt a number, (numeric keys invalid in XML) 25 | if(is_numeric($_key)) $key = 'element'; 26 | else if(!is_string($_key) || empty($_key) || strncmp($_key, '_', 1) === 0) continue; 27 | else $key = $_key; 28 | 29 | $xml->startElement($key); 30 | 31 | // if the key is numeric, add an ID attribute to make tags properly unique 32 | if(is_numeric($_key)) $xml->writeAttribute('id', $_key); 33 | 34 | // if the value is an array recurse into it 35 | if(is_array($value)) write($xml, $value); 36 | // otherwise write the text to the document 37 | else $xml->text($value); 38 | 39 | $xml->endElement(); 40 | } 41 | } 42 | 43 | // start the writing process 44 | write($xml, $data); 45 | 46 | $xml->endElement(); 47 | 48 | return $xml->outputMemory(); 49 | } 50 | 51 | public static function decode($contents) { 52 | 53 | if(!$contents) return array(); 54 | 55 | //Get the XML parser of PHP - PHP must have this module for the parser to work 56 | $parser = xml_parser_create(""); 57 | xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, "UTF-8"); 58 | # http://minutillo.com/steve/weblog/2004/6/17/php-xml-and-character-encodings-a-tale-of-sadness-rage-and-data-loss 59 | xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); 60 | xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1); 61 | xml_parse_into_struct($parser, trim($contents), $xml_values); 62 | xml_parser_free($parser); 63 | 64 | if(!$xml_values) return; 65 | 66 | //Initializations 67 | $xml_array = array(); 68 | 69 | $current = &$xml_array; 70 | 71 | //Go through the tags. 72 | foreach($xml_values as $data) { 73 | unset($attributes, $value); //Remove existing values 74 | // sets tag(string), type(string), level(int), attributes(array). 75 | extract($data); 76 | 77 | // Set value if it exists 78 | $result = null; 79 | if(isset($value)) 80 | $result = $value; 81 | 82 | //The starting of the tag '' 83 | if($type == self::TYPE_OPEN) { 84 | $parent[$level - 1] =& $current; 85 | // Insert a new tag 86 | if(!is_array($current) or (!in_array($tag, array_keys($current)))) { 87 | $current[$tag] = $result; 88 | $current =& $current[$tag]; 89 | } // A duplicate key exists, make it an array 90 | else { 91 | if(isset($current[$tag][0]) and is_array($current[$tag])) 92 | $current[$tag][] = $result; 93 | //This section will make the value an array if multiple tags with the same name appear together 94 | else 95 | $current[$tag] = array( 96 | $current[$tag], 97 | $result 98 | );//This will combine the existing item and the new item together to make an array 99 | 100 | $current = &$current[$tag][count($current[$tag]) - 1]; 101 | } 102 | 103 | } //Tags with no content '' 104 | else if($type == self::TYPE_COMPLETE) { 105 | //See if the key is already taken. 106 | if(!isset($current[$tag])) 107 | $current[$tag] = $result; 108 | else { 109 | //If it is already an array... 110 | if(isset($current[$tag][0]) and is_array($current[$tag])) 111 | $current[$tag][] = $result; 112 | //Make it an array using using the existing value and the new value 113 | else 114 | $current[$tag] = array($current[$tag], $result); 115 | } 116 | 117 | } //End of tag '' 118 | else if($type == self::TYPE_CLOSE) 119 | $current =& $parent[$level - 1]; 120 | } 121 | 122 | return $xml_array; 123 | } 124 | 125 | public static function arrayify($data, array $force = array(), $forceEach = true) { 126 | if(is_array($data) || is_object($data)) { 127 | $jsonarr = array(); 128 | if($data instanceof Model) 129 | $data = $data->_toArray(); 130 | foreach($data as $key => $value) { 131 | $jsonarr[$key] = self::arrayify($value, $forceEach ? $force : array(), false); 132 | } 133 | return $jsonarr; 134 | } else { 135 | return $data; 136 | } 137 | } 138 | 139 | } 140 | 141 | ?> -------------------------------------------------------------------------------- /src/Wave/DB/Generator.php: -------------------------------------------------------------------------------- 1 | getTables($database); 36 | 37 | $existing_files = glob(self::getModelPath($database) . '*.php'); 38 | 39 | foreach($tables as $table) { 40 | $base_file = self::getBaseModelPath($database) . $table->getClassName() . '.php'; 41 | $base_rendered = self::renderTemplate('base-model', array('table' => $table)); 42 | self::writeTemplateIfChanged($table, $base_file, $base_rendered); 43 | 44 | $stub_file = self::getModelPath($database) . $table->getClassName() . '.php'; 45 | if(!file_exists($stub_file)) 46 | file_put_contents($stub_file, self::renderTemplate('stub-model', array('table' => $table))); 47 | 48 | $current_files[] = $stub_file; 49 | } 50 | 51 | $orphans = array_diff($existing_files, $current_files); 52 | 53 | } 54 | } 55 | 56 | /** 57 | * @param string $filepath 58 | * @param string $content 59 | * 60 | * @return bool 61 | */ 62 | private static function writeTemplateIfChanged($table, $filepath, $contents) { 63 | 64 | if(file_exists($filepath)){ 65 | $rendered_fingerprint = $table->getSchemaFingerprint(); 66 | $current_contents = file_get_contents($filepath); 67 | preg_match('/@fingerprint: ([0-9a-f]{32})/', $current_contents, $matches); 68 | if(!isset($matches[1]) || $rendered_fingerprint !== $matches[1]){ 69 | Wave\Log::write('generator', sprintf('Table [%s] has changed, updating base model file...', $table->getName()), Wave\Log::DEBUG); 70 | file_put_contents($filepath, $contents); 71 | } 72 | } 73 | else { 74 | Wave\Log::write('generator', sprintf('Base Model for table [%s] doesn\'t exist, creating...', $table->getName()), Wave\Log::DEBUG); 75 | file_put_contents($filepath, $contents); 76 | } 77 | 78 | } 79 | 80 | /** 81 | * @param string $template 82 | * @param array $data 83 | * @param string $template_ext 84 | * 85 | * @return string 86 | */ 87 | private static function renderTemplate($template, $data, $template_ext = '.phpt') { 88 | 89 | $loaded_template = self::$twig->load($template . $template_ext); 90 | return $loaded_template->render($data); 91 | 92 | } 93 | 94 | private static function initTwig() { 95 | 96 | $loader = new FilesystemLoader(__DIR__ . DS . 'Generator' . DS . 'Templates'); 97 | self::$twig = new Environment($loader, array('autoescape' => false)); 98 | self::$twig->addFilter(new TwigFilter('addslashes', 'addslashes')); 99 | self::$twig->addFilter( 100 | new TwigFilter('export', 101 | function ($var) { 102 | return var_export($var, true); 103 | } 104 | ) 105 | ); 106 | self::$twig->addFilter(new TwigFilter('implode', 'implode')); 107 | self::$twig->addFilter(new TwigFilter('singularize', '\\Wave\\Inflector::singularize')); 108 | self::$twig->addFilter(new TwigFilter('formatType', '\\Wave\\DB\\Generator::formatTypeForSource')); 109 | self::$twig->addGlobal('baseModelClass', static::$baseModelClass); 110 | } 111 | 112 | public static function formatTypeForSource($type) { 113 | 114 | if(null === $type) 115 | return "null"; 116 | else if(is_int($type) || is_float($type)) 117 | return "$type"; 118 | else if(is_bool($type)) 119 | return $type ? "true" : "false"; 120 | else 121 | return "'$type'"; 122 | 123 | } 124 | 125 | /** 126 | * @param Wave\DB $database 127 | */ 128 | private static function createModelDirectory(Wave\DB $database) { 129 | 130 | $basedir = self::getBaseModelPath($database); 131 | 132 | if(!file_exists($basedir)) 133 | mkdir($basedir, 0775, true); 134 | } 135 | 136 | /** 137 | * @param \Wave\DB $database 138 | * 139 | * @return string 140 | */ 141 | private static function getBaseModelPath(Wave\DB $database) { 142 | return self::getModelPath($database) . 'Base' . DS; 143 | } 144 | 145 | /** 146 | * @param \Wave\DB $database 147 | * 148 | * @return string 149 | */ 150 | private static function getModelPath(Wave\DB $database) { 151 | 152 | $namespace = $database->getNamespace(false); 153 | $model_directory = Wave\Config::get('wave')->path->models; 154 | 155 | return $model_directory . DS . $namespace . DS; 156 | 157 | } 158 | 159 | public static function setBaseModelClass($baseModelClass) { 160 | self::$baseModelClass = $baseModelClass; 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/Wave/Log.php: -------------------------------------------------------------------------------- 1 | logger->file->level; 67 | 68 | return static::$default_level; 69 | } 70 | 71 | public static function getDefaultHandlers() { 72 | if(static::$default_handlers === null) { 73 | 74 | static::$default_handlers = array(); 75 | 76 | $log_path = Config::get('wave')->path->logs; 77 | $log_path .= Config::get('wave')->logger->file->file; 78 | $log_dir = dirname($log_path); 79 | 80 | 81 | if(!is_writable($log_dir)) { 82 | @mkdir($log_dir, 0770, true); 83 | } 84 | 85 | $stream_handler = new StreamHandler($log_path, static::getDefaultLevel()); 86 | $stream_handler->pushProcessor(new ExceptionIntrospectionProcessor()); 87 | 88 | static::pushDefaultHandler($stream_handler); 89 | 90 | if(PHP_SAPI === 'cli') { 91 | $cli_handler = new CliHandler(Config::get('wave')->logger->cli->level); 92 | $cli_handler->setFormatter(new LineFormatter(CliHandler::LINE_FORMAT)); 93 | static::pushDefaultHandler($cli_handler); 94 | } 95 | 96 | } 97 | 98 | return static::$default_handlers; 99 | } 100 | 101 | public static function pushDefaultHandler(AbstractHandler $handler) { 102 | if(static::$default_handlers === null) { 103 | static::$default_handlers = self::getDefaultHandlers(); 104 | } 105 | 106 | array_unshift(static::$default_handlers, $handler); 107 | } 108 | 109 | /** 110 | * Create a new channel with the specified Handler 111 | * 112 | * If a `$handler` is not specified it will set a default StreamHandler 113 | * to the logfile specified in the `wave.php` configuration file. 114 | * 115 | * @param string $channel The name of the channel to return 116 | * @param array $handlers Any handlers to attach to the channel [optional] 117 | * @param bool $use_default_handlers 118 | * 119 | * @return \Monolog\Logger A new Logger instance 120 | */ 121 | public static function createChannel($channel, array $handlers = array(), $use_default_handlers = true) { 122 | if($use_default_handlers) 123 | $handlers += static::getDefaultHandlers(); 124 | 125 | static::$channels[$channel] = new Logger($channel, $handlers); 126 | return static::$channels[$channel]; 127 | } 128 | 129 | /** 130 | * @param string $name 131 | * @param bool $create Create the channel if it does not exist (default=true) 132 | * 133 | * @return \Monolog\Logger A Logger instance for the given channel or `null` if not found 134 | */ 135 | public static function getChannel($name, $create = true) { 136 | if(!isset(static::$channels[$name])) { 137 | if($create === true) return static::createChannel($name); 138 | else return null; 139 | } 140 | return static::$channels[$name]; 141 | } 142 | 143 | /** 144 | * Set a Logger instance for a channel 145 | * 146 | * @param string $name The channel name to set to 147 | * @param \Monolog\Logger $instance 148 | * 149 | * @return \Monolog\Logger 150 | */ 151 | public static function setChannel($name, Logger $instance) { 152 | return static::$channels[$name] = $instance; 153 | } 154 | 155 | /** 156 | * A shorthand for writing a message to a given channel 157 | * 158 | * @param string $channel The channel to write to 159 | * @param string $message The message to write 160 | * @param int $level The level of the message (debug, info, notice, warning, error, critical) 161 | * 162 | * @return Bool Whether the message has been written 163 | **/ 164 | public static function write($channel, $message, $level = Logger::INFO, $context = array()) { 165 | $channel = static::getChannel($channel); 166 | 167 | return $channel->addRecord($level, $message, $context); 168 | } 169 | 170 | } -------------------------------------------------------------------------------- /src/Wave/Http/HeaderBag.php: -------------------------------------------------------------------------------- 1 | add($headers); 30 | } 31 | 32 | /** 33 | * Returns the headers. 34 | * @return array An array of headers 35 | */ 36 | public function all() { 37 | return $this->headers; 38 | } 39 | 40 | /** 41 | * Returns the parameter keys. 42 | * @return array An array of parameter keys 43 | */ 44 | public function keys() { 45 | return array_keys($this->headers); 46 | } 47 | 48 | /** 49 | * Replaces the current HTTP headers by a new set. 50 | * @param array $headers An array of HTTP headers 51 | */ 52 | public function replace(array $headers = array()) { 53 | $this->headers = array(); 54 | $this->add($headers); 55 | } 56 | 57 | /** 58 | * Adds new headers the current HTTP headers set. 59 | * 60 | * @param array $headers An array of HTTP headers 61 | * 62 | * @api 63 | */ 64 | public function add(array $headers) { 65 | foreach($headers as $key => $values) { 66 | $this->set($key, $values); 67 | } 68 | } 69 | 70 | /** 71 | * Returns a header value by name. 72 | * 73 | * @param string $key The header name 74 | * @param mixed $default The default value 75 | * @param Boolean $first Whether to return the first value or all header values 76 | * 77 | * @return string|array The first header value if $first is true, an array of values otherwise 78 | */ 79 | public function get($key, $default = null, $first = true) { 80 | 81 | $key = strtr(strtolower($key), '_', '-'); 82 | 83 | if(!array_key_exists($key, $this->headers)) { 84 | if(null === $default) { 85 | return $first ? null : array(); 86 | } 87 | 88 | return $first ? $default : array($default); 89 | } 90 | 91 | if($first) { 92 | return count($this->headers[$key]) ? $this->headers[$key][0] : $default; 93 | } 94 | 95 | return $this->headers[$key]; 96 | } 97 | 98 | /** 99 | * Sets a header by name. 100 | * 101 | * @param string $key The key 102 | * @param string|array $values The value or an array of values 103 | * @param Boolean $replace Whether to replace the actual value or not (true by default) 104 | * 105 | * @api 106 | */ 107 | public function set($key, $values, $replace = true) { 108 | 109 | $key = strtr(strtolower($key), '_', '-'); 110 | 111 | $values = array_values((array) $values); 112 | 113 | if(true === $replace || !isset($this->headers[$key])) { 114 | $this->headers[$key] = $values; 115 | } else { 116 | $this->headers[$key] = array_merge($this->headers[$key], $values); 117 | } 118 | } 119 | 120 | /** 121 | * Returns true if the HTTP header is defined. 122 | * 123 | * @param string $key The HTTP header 124 | * 125 | * @return Boolean true if the parameter exists, false otherwise 126 | * 127 | * @api 128 | */ 129 | public function has($key) { 130 | return array_key_exists(strtr(strtolower($key), '_', '-'), $this->headers); 131 | } 132 | 133 | /** 134 | * Returns true if the given HTTP header contains the given value. 135 | * 136 | * @param string $key The HTTP header name 137 | * @param string $value The HTTP value 138 | * 139 | * @return Boolean true if the value is contained in the header, false otherwise 140 | * 141 | * @api 142 | */ 143 | public function contains($key, $value) { 144 | return in_array($value, $this->get($key, null, false)); 145 | } 146 | 147 | /** 148 | * Removes a header. 149 | * 150 | * @param string $key The HTTP header name 151 | * 152 | * @api 153 | */ 154 | public function remove($key) { 155 | $key = strtr(strtolower($key), '_', '-'); 156 | 157 | unset($this->headers[$key]); 158 | } 159 | 160 | /** 161 | * Returns an iterator for headers. 162 | * 163 | * @return \ArrayIterator An \ArrayIterator instance 164 | */ 165 | public function getIterator(): \ArrayIterator { 166 | return new \ArrayIterator($this->headers); 167 | } 168 | 169 | /** 170 | * Returns the number of headers. 171 | * 172 | * @return int The number of headers 173 | */ 174 | public function count(): int { 175 | return count($this->headers); 176 | } 177 | 178 | /** 179 | * Returns the headers as a string. 180 | * 181 | * @return string The headers 182 | */ 183 | public function __toString() { 184 | if(!$this->headers) { 185 | return ''; 186 | } 187 | 188 | $max = max(array_map('strlen', array_keys($this->headers))) + 1; 189 | $content = ''; 190 | ksort($this->headers); 191 | foreach($this->headers as $name => $values) { 192 | $name = implode('-', array_map('ucfirst', explode('-', $name))); 193 | foreach($values as $value) { 194 | $content .= sprintf("%-{$max}s %s\r\n", $name . ':', $value); 195 | } 196 | } 197 | 198 | return $content; 199 | } 200 | 201 | } -------------------------------------------------------------------------------- /src/Wave/View.php: -------------------------------------------------------------------------------- 1 | path->views); 27 | 28 | $conf = array('cache' => Config::get('wave')->view->cache); 29 | if(Core::$_MODE !== Core::MODE_PRODUCTION) { 30 | $conf['auto_reload'] = true; 31 | $conf['debug'] = true; 32 | } 33 | $this->twig = new View\TwigEnvironment($loader, $conf); 34 | $this->twig->addExtension(new View\TwigExtension()); 35 | foreach(self::$_filters as $name => $action) { 36 | if ($action instanceof TwigFilter) { 37 | $this->twig->addFilter($action); 38 | continue; 39 | } 40 | 41 | $this->twig->addFilter(new TwigFilter($name, $action)); 42 | } 43 | 44 | $this->twig->registerUndefinedFilterCallback( 45 | function ($name) { 46 | if(function_exists($name)) { 47 | return new TwigFunction($name); 48 | } 49 | 50 | return false; 51 | } 52 | ); 53 | $this->twig->addFilter(new TwigFilter('last','\\Wave\\Utils::array_peek')); 54 | $this->twig->addFilter(new TwigFilter('short', 55 | '\\Wave\\Utils::shorten', array( 56 | 'pre_escape' => 'html', 57 | 'is_safe' => array('html') 58 | ) 59 | ) 60 | ); 61 | 62 | // global variables 63 | $this->twig->addGlobal('_assets', Config::get('deploy')->assets); 64 | $this->twig->addGlobal('_host', Config::get('deploy')->profiles->default->baseurl); 65 | $this->twig->addGlobal('_mode', Core::$_MODE); 66 | 67 | if(self::$_timezone !== null) 68 | $this->twig->getExtension(CoreExtension::class)->setTimezone(self::$_timezone); 69 | 70 | if(self::$_date_format !== null) 71 | $this->twig->getExtension(CoreExtension::class)->setDateFormat(self::$_date_format); 72 | 73 | if(Config::get('deploy')->mode == Core::MODE_DEVELOPMENT || isset($_REQUEST['_wave_show_debugger'])) 74 | $this->twig->addGlobal('_debugger', Debug::getInstance()); 75 | 76 | foreach(self::$_globals as $key => $value) 77 | $this->twig->addGlobal($key, $value); 78 | 79 | } 80 | 81 | public static function getInstance() { 82 | 83 | if(self::$instance === null) 84 | self::$instance = new self(); 85 | 86 | return self::$instance; 87 | } 88 | 89 | 90 | public function render($template, $data = array()) { 91 | 92 | // locate the template file 93 | $template .= Config::get('wave')->view->extension; 94 | Hook::triggerAction('view.before_load_template', array(&$this, &$template)); 95 | $loaded_template = $this->twig->load($template); 96 | Hook::triggerAction('view.before_render', array(&$this, &$data)); 97 | $html = $loaded_template->render($data); 98 | Hook::triggerAction('view.after_render', array(&$this, &$html)); 99 | return $html; 100 | 101 | } 102 | 103 | public static function registerFilter($filter, $action) { 104 | if(self::$instance == null) self::$_filters[$filter] = $action; 105 | else self::$instance->twig->addFilter($filter, $action); 106 | } 107 | 108 | public static function registerGlobal($name, $value) { 109 | if(self::$instance == null) self::$_globals[$name] = $value; 110 | else self::$instance->twig->addGlobal($name, $value); 111 | 112 | } 113 | 114 | public static function setTimezone($timezone) { 115 | if(self::$instance == null) self::$_timezone = $timezone; 116 | else self::$instance->twig->getExtension(CoreExtension::class)->setTimezone($timezone); 117 | } 118 | 119 | public static function setDefaultDateFormat($format) { 120 | if(self::$instance == null) self::$_date_format = $format; 121 | else self::$instance->twig->getExtension(CoreExtension::class)->setDateFormat($format); 122 | } 123 | 124 | public static function generate() { 125 | 126 | $cache_dir = Config::get('wave')->view->cache; 127 | if(!file_exists($cache_dir)) 128 | @mkdir($cache_dir, 0770, true); 129 | 130 | if(!file_exists($cache_dir)) 131 | throw new Exception('Could not generate views, the cache directory does not exist or is not writable'); 132 | 133 | // delete caches 134 | $dir_iterator = new RecursiveDirectoryIterator($cache_dir); 135 | $iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::CHILD_FIRST); 136 | foreach($iterator as $file) { 137 | if($file->getFilename() === '.' || $file->getFilename() === '..') { 138 | continue; 139 | } 140 | $func = $file->isDir() ? 'rmdir' : 'unlink'; 141 | $func($file->getRealPath()); 142 | } 143 | $self = self::getInstance(); 144 | 145 | $source_path = Config::get('wave')->path->views; 146 | $dir_iterator = new RecursiveDirectoryIterator($source_path); 147 | $iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::CHILD_FIRST); 148 | $l = strlen($source_path); 149 | foreach($iterator as $template) { 150 | $filename = $template->getFilename(); 151 | if(pathinfo($filename, PATHINFO_EXTENSION) != 'phtml') continue; 152 | $self->twig->load(substr($template, $l)); 153 | } 154 | 155 | } 156 | 157 | } 158 | 159 | 160 | ?> -------------------------------------------------------------------------------- /src/Wave/DB/Generator/Templates/base-model.phpt: -------------------------------------------------------------------------------- 1 | array( 52 | 'default' => {{ column.Default|formatType }}, 53 | 'data_type' => {{ column.DataType }}, 54 | 'nullable' => {% if column.isNullable %}true{% else %}false{% endif %}, 55 | 'serial' => {% if column.isSerial %}true{% else %}false{% endif %}, 56 | 'generated' => {% if column.isGenerated %}true{% else %}false{% endif %}, 57 | 'sequence' => {% if column.SequenceName %}'{{ column.SequenceName }}'{% else %}null{% endif %}, 58 | 'metadata' => array({% for key, value in column.Metadata %}{{key|export}} =>{{value|export}}{% if not loop.last %},{% endif %} {% endfor %}) 59 | 60 | ){% if not loop.last %},{% endif %} 61 | 62 | {% endfor %}); 63 | 64 | //Indexes 65 | protected static $_constraints = array( 66 | {% for constraint in table.Constraints %}{% if constraint.Type == constant('\\Wave\\DB\\Constraint::TYPE_PRIMARY') or constraint.Type == constant('\\Wave\\DB\\Constraint::TYPE_UNIQUE') %} 67 | 68 | '{{ constraint.Name }}' => array( 69 | 'type' => {{ constraint.Type }}, 70 | 'fields' => array({% for column in constraint.Columns %}'{{ column.Name }}'{% if not loop.last %}, {% endif %}{% endfor %}) 71 | 72 | ){% if not loop.last %},{% endif %} 73 | 74 | {% endif %}{% endfor %}); 75 | 76 | //Relations 77 | protected static $_relations = array( 78 | {% for relation in table.Relations %}{% if relation.Type != constant('\\Wave\\DB\\Relation::RELATION_UNKNOWN') %} 79 | 80 | //{{ relation.Description }} 81 | '{{ relation.Name }}' => array( 82 | 'relation_type' => {{ relation.Type }}, 83 | 'local_columns' => array({% for column in relation.LocalColumns %}'{{ column.Name }}'{% if not loop.last %},{% endif %}{% endfor %}), 84 | 'related_class' => '{{ relation.ReferencedTable.getClassName(true)|addslashes }}', 85 | 'related_columns' => array({% for column in relation.ReferencedColumns %}'{{ column.Name }}'{% if not loop.last %},{% endif %}{% endfor %}), 86 | {% if relation.Type == constant('\\Wave\\DB\\Relation::MANY_TO_MANY') %}'target_relation' => array( 87 | 'local_columns' => array({% for column in relation.TargetRelation.LocalColumns %}'{{ column.Name }}'{% if not loop.last %},{% endif %}{% endfor %}), 88 | 'related_class' => '{{ relation.TargetRelation.ReferencedTable.getClassName(true)|addslashes }}', 89 | 'related_columns' => array({% for column in relation.TargetRelation.ReferencedColumns %}'{{ column.Name }}'{% if not loop.last %},{% endif %}{% endfor %}), 90 | ) 91 | {% endif %} 92 | 93 | ), 94 | 95 | {% endif %}{% endfor %}); 96 | 97 | {% for column in table.Columns %} 98 | 99 | //{{ column.TypeDescription|raw }} {{ column.Extra }} {{ column.Comment }} 100 | public function get{{ column.Name }}() 101 | { 102 | return $this->_data['{{ column.Name }}']; 103 | } 104 | 105 | public function set{{ column.Name }}($value) 106 | { 107 | $this->_data['{{ column.Name }}'] = $value; 108 | $this->_dirty['{{ column.Name }}'] = true; 109 | return $this; 110 | } 111 | {% endfor %} 112 | 113 | //Relations 114 | {% for relation in table.Relations %} 115 | 116 | {% if relation.Type == constant('\\Wave\\DB\\Relation::ONE_TO_ONE') %}{% include 'relations/one-to-one.phpt' %} 117 | {% elseif relation.Type == constant('\\Wave\\DB\\Relation::ONE_TO_MANY') %}{% include 'relations/one-to-many.phpt' %} 118 | {% elseif relation.Type == constant('\\Wave\\DB\\Relation::MANY_TO_ONE') %}{% include 'relations/many-to-one.phpt' %} 119 | {% elseif relation.Type == constant('\\Wave\\DB\\Relation::MANY_TO_MANY') %}{% include 'relations/many-to-many.phpt' %} 120 | {% endif %} 121 | 122 | {% endfor %} 123 | 124 | 125 | } -------------------------------------------------------------------------------- /src/Wave/DB/Column.php: -------------------------------------------------------------------------------- 1 | table = $table; 51 | $this->name = $name; 52 | $this->nullable = $nullable; 53 | $this->data_type = $data_type; 54 | $this->default = $default_value; 55 | $this->is_serial = $is_serial; 56 | $this->type_desc = $type_desc; 57 | $this->extra = $extra; 58 | $this->comment = $comment; 59 | 60 | $this->parseMetadata($comment); 61 | } 62 | 63 | /** 64 | * @return Table 65 | */ 66 | public function getTable() { 67 | return $this->table; 68 | } 69 | 70 | /** 71 | * @param bool $escape 72 | * 73 | * @return string 74 | */ 75 | public function getName($escape = false) { 76 | return $escape ? $this->table->getDatabase()->escape($this->name) : $this->name; 77 | } 78 | 79 | /** 80 | * @return bool 81 | */ 82 | public function isNullable() { 83 | return $this->nullable; 84 | } 85 | 86 | /** 87 | * @return boolean 88 | */ 89 | public function isSerial() { 90 | return $this->is_serial; 91 | } 92 | 93 | /** 94 | * @return int 95 | */ 96 | public function getDataType() { 97 | return $this->data_type; 98 | } 99 | 100 | /** 101 | * Return the datatype for this column as a PHP compatible type 102 | * @return string 103 | */ 104 | public function getDataTypeAsPHPType() { 105 | switch($this->data_type) { 106 | case self::TYPE_INT: 107 | return 'int'; 108 | case self::TYPE_STRING: 109 | return 'string'; 110 | case self::TYPE_BOOL: 111 | return 'bool'; 112 | case self::TYPE_DATE: 113 | case self::TYPE_TIMESTAMP: 114 | return '\\DateTime'; 115 | case self::TYPE_FLOAT: 116 | return 'float'; 117 | default: 118 | return 'mixed'; 119 | } 120 | } 121 | 122 | /** 123 | * @return mixed 124 | */ 125 | public function getDefault() { 126 | return $this->default; 127 | } 128 | 129 | /** 130 | * @return string 131 | */ 132 | public function getTypeDescription() { 133 | return $this->type_desc; 134 | } 135 | 136 | /** 137 | * @return string 138 | */ 139 | public function getExtra() { 140 | return $this->extra; 141 | } 142 | 143 | /** 144 | * @return string 145 | */ 146 | public function getComment() { 147 | return $this->comment; 148 | } 149 | 150 | /** 151 | * @param $key 152 | * @return null 153 | */ 154 | public function getMetadata($key = null) { 155 | if($key !== null) { 156 | if(array_key_exists($key, $this->metadata)) 157 | return $this->metadata[$key]; 158 | else 159 | return null; 160 | } 161 | return $this->metadata; 162 | } 163 | 164 | /** 165 | * @return bool 166 | */ 167 | public function isPrimaryKey() { 168 | $pk = $this->table->getPrimaryKey(); 169 | 170 | return $pk === null ? false : in_array($this, $pk->getColumns()); 171 | } 172 | 173 | /** 174 | * @return bool 175 | */ 176 | public function isGenerated(): bool 177 | { 178 | return strpos($this->getExtra(), 'GENERATED') !== false; 179 | } 180 | 181 | private function parseMetadata($raw) { 182 | 183 | $parsed = json_decode($raw, true); 184 | if(json_last_error() === JSON_ERROR_NONE && is_array($parsed)) { 185 | $this->metadata = $parsed; 186 | } 187 | 188 | } 189 | 190 | /** 191 | * @return string 192 | */ 193 | public function getSequenceName() { 194 | return $this->sequence_name; 195 | } 196 | 197 | /** 198 | * @param string $sequence_name 199 | */ 200 | public function setSequenceName($sequence_name) { 201 | $this->sequence_name = $sequence_name; 202 | } 203 | 204 | /** 205 | * Provide a representation of this column that can be used to calculate a 206 | * fingerprint for whether it has changed or not. 207 | */ 208 | public function __serialize() { 209 | return [ 210 | 'table' => $this->table->getName(), 211 | 'name' => $this->getName(), 212 | 'nullable' => $this->isNullable(), 213 | 'data_type' => $this->getDataType(), 214 | 'default' => $this->getDefault(), 215 | 'serial' => $this->isSerial(), 216 | 'type_desc' => $this->getTypeDescription(), 217 | 'generated' => $this->isGenerated(), 218 | 'extra' => $this->getExtra(), 219 | 'comment' => $this->getComment() 220 | ]; 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /src/Wave/Router.php: -------------------------------------------------------------------------------- 1 | profiles->default->baseurl; 38 | } 39 | $instance = new self($host); 40 | Hook::triggerAction('router.after_init', array(&$instance)); 41 | return $instance; 42 | } 43 | 44 | public static function getActionByName($profile, $action_name) { 45 | $table = static::getRoutesTableCache($profile); 46 | $action_name = ltrim($action_name, '\\'); 47 | 48 | return isset($table[$action_name]) ? $table[$action_name] : null; 49 | } 50 | 51 | public static function getProfiles() { 52 | if(!isset(self::$profiles)) 53 | self::$profiles = Config::get('deploy')->profiles; 54 | 55 | return self::$profiles; 56 | } 57 | 58 | public static function getCacheName($host, $type) { 59 | return "routes/$host.$type"; 60 | } 61 | 62 | /** 63 | * Return all routes that have a schedule expression 64 | * 65 | * @return Action[] 66 | * @throws RoutingException 67 | */ 68 | public function getScheduledRoutes() 69 | { 70 | $table = static::getRoutesTableCache($this->profile); 71 | 72 | return array_filter($table, function (Action $action){ 73 | return $action->hasSchedule(); 74 | }); 75 | } 76 | 77 | public static function getRoutesTreeCache($profile) { 78 | $root = Cache::load(self::getCacheName($profile, 'tree')); 79 | if($root == null) { 80 | $root = Cache::load(self::getCacheName('default', 'tree')); 81 | } 82 | 83 | if(!($root instanceof Node)) 84 | throw new RoutingException("Could not load route tree for profile: {$profile} nor default profile"); 85 | 86 | return $root; 87 | } 88 | 89 | public static function getRoutesTableCache($profile) { 90 | $table = Cache::load(self::getCacheName($profile, 'table')); 91 | if($table == null) { 92 | $table = Cache::load(self::getCacheName('default', 'table')); 93 | } 94 | 95 | if(!is_array($table)) 96 | throw new RoutingException("Could not load routes table for profile: {$profile} nor default profile"); 97 | 98 | return $table; 99 | } 100 | 101 | public function __construct($profile) { 102 | if(isset(static::getProfiles()->$profile)) { 103 | $this->profile = $profile; 104 | } else { 105 | // try looking for the profile using the baseurl instead 106 | foreach(static::getProfiles() as $name => $config) { 107 | if($config->baseurl == $profile) { 108 | $this->profile = $name; 109 | break; 110 | } 111 | } 112 | } 113 | 114 | if(!isset($this->profile)) { 115 | throw new RoutingException("Unknown routing profile {$profile}"); 116 | } 117 | } 118 | 119 | /** 120 | * @param Request $request 121 | * 122 | * @throws \LogicException 123 | * @throws Http\Exception\NotFoundException 124 | * @return Response 125 | */ 126 | public function route(?Request $request = null) { 127 | 128 | if(null === $request) 129 | $request = Request::createFromGlobals(); 130 | 131 | $this->request = $request; 132 | 133 | $this->request_uri = $request->getPath(); 134 | if(strrpos($this->request_uri, $request->getFormat()) !== false) { 135 | $this->request_uri = substr($this->request_uri, 0, -(strlen($request->getFormat()) + 1)); 136 | } 137 | $this->request_method = $request->getMethod(); 138 | 139 | Hook::triggerAction('router.before_routing', array(&$this)); 140 | 141 | $url = $this->request_method . $this->request_uri; 142 | $node = $this->getRootNode()->findChild($url, $this->request); 143 | 144 | /** @var \Wave\Router\Action $action */ 145 | if($node instanceof Router\Node && $action = $node->getAction()) { 146 | Hook::triggerAction('router.before_invoke', array(&$action, &$this)); 147 | $this->request->setAction($action); 148 | $this->response = Controller::invoke($action, $this->request); 149 | Hook::triggerAction('router.before_response', array(&$action, &$this)); 150 | if(!($this->response instanceof Response)) { 151 | throw new \LogicException("Action {$action->getAction()} should return a \\Wave\\Http\\Response object", 500); 152 | } else { 153 | return $this->response->prepare($this->request); 154 | } 155 | } else 156 | throw new NotFoundException('The requested URL ' . $url . ' does not exist', $this->request); 157 | } 158 | 159 | public function getRootNode() { 160 | if(!($this->root instanceof Node)) { 161 | $this->root = static::getRoutesTreeCache($this->profile); 162 | } 163 | return $this->root; 164 | } 165 | 166 | /** 167 | * @return \Wave\Http\Request 168 | */ 169 | public function getRequest() { 170 | return $this->request; 171 | } 172 | 173 | /** 174 | * @param \Wave\Http\Request $request 175 | */ 176 | public function setRequest($request) { 177 | $this->request = $request; 178 | } 179 | 180 | /** 181 | * @return \Wave\Http\Response 182 | */ 183 | public function getResponse() { 184 | return $this->response; 185 | } 186 | } 187 | 188 | ?> 189 | -------------------------------------------------------------------------------- /src/Wave/Reflector.php: -------------------------------------------------------------------------------- 1 | _classes = array(); 23 | if($type == self::SCAN_FILE && is_file($handle)) { 24 | $class = self::findClass($handle); 25 | if($class !== false) 26 | $this->_classes[] = $class; 27 | } else if($type == self::SCAN_DIRECTORY && is_dir($handle)) { 28 | 29 | if($recursive === true) { 30 | $dir_iterator = new \RecursiveDirectoryIterator($handle); 31 | $iterator = new \RecursiveIteratorIterator($dir_iterator, \RecursiveIteratorIterator::SELF_FIRST); 32 | } else { 33 | $dir_iterator = new \FilesystemIterator($handle); 34 | $iterator = new \IteratorIterator($dir_iterator, \RecursiveIteratorIterator::SELF_FIRST); 35 | } 36 | foreach($iterator as $file) { 37 | $class = self::findClass($file); 38 | if($class !== false) 39 | $this->_classes[] = $class; 40 | } 41 | } 42 | 43 | } 44 | 45 | public function execute($include_inherited_members = false) { 46 | if(!isset($this->_classes[0])) 47 | throw new \Wave\Exception('No files to reflect on'); 48 | 49 | $classes = array(); 50 | 51 | foreach($this->_classes as $class) { 52 | $reflector = new \ReflectionClass($class); 53 | 54 | $class_annotations = Annotation::parse($reflector->getDocComment(), $class); 55 | $parent_class = $reflector->getParentClass(); 56 | 57 | $c = array( 58 | 'name' => $class, 59 | 'subclasses' => $parent_class instanceof \ReflectionClass ? $parent_class->getName() : '', 60 | 'implements' => $reflector->getInterfaceNames(), 61 | 'annotations' => $class_annotations 62 | ); 63 | 64 | $methods = array(); 65 | foreach($reflector->getMethods() as $method) { 66 | $declaring_class = $method->getDeclaringClass()->getName(); 67 | // don't put inherited methods on here plz 68 | if($declaring_class != $class && !$include_inherited_members) 69 | continue; 70 | 71 | $annotations = Annotation::parse($method->getDocComment(), $class); 72 | $method_annotations = array(); 73 | foreach($annotations as $annotation) { 74 | $method_annotations[] = $annotation; 75 | } 76 | 77 | $method_name = $method->getName(); 78 | $visibility = $method->isPublic() ? self::VISIBILITY_PUBLIC : ($method->isPrivate() ? self::VISIBILITY_PRIVATE : self::VISIBILITY_PROTECTED); 79 | $methods[$method_name] = array( 80 | 'name' => $method_name, 81 | 'visibility' => $visibility, 82 | 'static' => $method->isStatic(), 83 | 'parameters' => $method->getParameters(), 84 | 'annotations' => $method_annotations, 85 | 'declaring_class' => $method->getDeclaringClass()->getName() 86 | ); 87 | } 88 | 89 | $properties = array(); 90 | foreach($reflector->getProperties() as $property) { 91 | $declaring_class = $property->getDeclaringClass()->getName(); 92 | // don't put inherited methods on here plz 93 | if($declaring_class != $class && !$include_inherited_members) 94 | continue; 95 | 96 | $annotations = Annotation::parse($property->getDocComment(), $class); 97 | $property_annotations = array(); 98 | foreach($annotations as $annotation) { 99 | $property_annotations[] = $annotation; 100 | } 101 | 102 | $property_name = $property->getName(); 103 | $visibility = $property->isPublic() ? self::VISIBILITY_PUBLIC : ($property->isPrivate() ? self::VISIBILITY_PRIVATE : self::VISIBILITY_PROTECTED); 104 | $properties[$property_name] = array( 105 | 'name' => $property_name, 106 | 'visibility' => $visibility, 107 | 'static' => $property->isStatic(), 108 | 'annotations' => $property_annotations, 109 | 'declaring_class' => $property->getDeclaringClass()->getName() 110 | ); 111 | } 112 | 113 | $classes[$class] = array('class' => $c, 'methods' => $methods, 'properties' => $properties); 114 | 115 | } 116 | return $classes; 117 | 118 | } 119 | 120 | /** 121 | * Returns the full class name for the first class in the file. 122 | * 123 | * @param string $file A PHP file path 124 | * 125 | * @return string|false Full class name if found, false otherwise 126 | */ 127 | protected function findClass($file) { 128 | 129 | // skip anything that's not a regular file 130 | if (!is_file($file)) 131 | return false; 132 | 133 | $class = false; 134 | $namespace = false; 135 | $tokens = token_get_all(file_get_contents($file)); 136 | for($i = 0, $count = count($tokens); $i < $count; $i++) { 137 | $token = $tokens[$i]; 138 | 139 | if(!is_array($token)) { 140 | continue; 141 | } 142 | 143 | if(true === $class && T_STRING === $token[0]) { 144 | return $namespace . '\\' . $token[1]; 145 | } 146 | 147 | if(true === $namespace && (T_NAME_QUALIFIED === $token[0] || T_STRING === $token[0])) { 148 | $namespace = $token[1]; 149 | } 150 | 151 | if(T_CLASS === $token[0]) { 152 | $class = true; 153 | } 154 | 155 | if(T_NAMESPACE === $token[0]) { 156 | $namespace = true; 157 | } 158 | } 159 | 160 | return false; 161 | } 162 | 163 | } 164 | 165 | ?> 166 | -------------------------------------------------------------------------------- /src/Wave/Exception.php: -------------------------------------------------------------------------------- 1 | Log::CRITICAL, 14 | E_WARNING => Log::WARNING, 15 | E_PARSE => Log::EMERGENCY, 16 | E_NOTICE => Log::WARNING, 17 | E_CORE_ERROR => Log::CRITICAL, 18 | E_CORE_WARNING => Log::WARNING, 19 | E_COMPILE_ERROR => Log::CRITICAL, 20 | E_COMPILE_WARNING => Log::WARNING, 21 | E_USER_ERROR => Log::ERROR, 22 | E_USER_WARNING => Log::WARNING, 23 | E_USER_NOTICE => Log::WARNING, 24 | E_RECOVERABLE_ERROR => Log::ERROR, 25 | E_DEPRECATED => Log::WARNING, 26 | E_USER_DEPRECATED => Log::WARNING, 27 | ); 28 | 29 | private static $_reserved_memory = ''; 30 | private static $_controller = null; 31 | public static $_response_method = null; 32 | 33 | /** @var \Wave\Http\Request $request */ 34 | private static $request; 35 | /** @var \Wave\Http\Response $response */ 36 | private static $response; 37 | private static $_error_reporting_types; 38 | private static $working_dir; 39 | 40 | public static function register($controller) { 41 | if(!class_exists($controller) || !is_subclass_of($controller, '\\Wave\\Controller')) { 42 | throw new \Exception("Controller $controller must be an instance of \\Wave\\Controller"); 43 | } 44 | 45 | self::$_controller = "$controller.execute"; 46 | set_exception_handler(array('\\Wave\\Exception', 'handle')); 47 | 48 | Hook::registerHandler( 49 | 'router.before_routing', function (Router $router) { 50 | if(Exception::$_response_method === null) 51 | Exception::$_response_method = $router->getRequest()->getFormat(); 52 | 53 | Exception::setRequest($router->getRequest()); 54 | } 55 | ); 56 | } 57 | 58 | public static function registerError($error_types = -1, $reserved_memory = 10) { 59 | self::$working_dir = getcwd(); 60 | set_error_handler(array('\\Wave\\Exception', 'handleError')); 61 | register_shutdown_function(array('\\Wave\\Exception', 'handleFatalError')); 62 | self::$_error_reporting_types = $error_types; 63 | self::$_reserved_memory = str_repeat('x', 1024 * $reserved_memory); 64 | } 65 | 66 | public static function handleError($code, $message, $file = null, $line = 0) { 67 | if(!(self::$_error_reporting_types & $code & error_reporting())) { 68 | return true; 69 | } 70 | 71 | throw new ErrorException($message, $code, $code, $file, $line); 72 | } 73 | 74 | public static function handleFatalError() { 75 | if(null === $lastError = error_get_last()) { 76 | return; 77 | } 78 | 79 | chdir(self::$working_dir); 80 | 81 | self::$_reserved_memory = null; 82 | 83 | $errors = E_ERROR | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING; 84 | 85 | if($lastError['type'] & $errors) { 86 | self::handleError(@$lastError['type'], @$lastError['message'], @$lastError['file'], @$lastError['line']); 87 | } 88 | } 89 | 90 | public static function handle($e, $send_response = true) { 91 | try { 92 | Hook::triggerAction('exception.handle', array(&$e)); 93 | 94 | $log_message = sprintf('%-4s %s', "({$e->getCode()})", $e->getMessage()); 95 | // get the channel manually so the introspection works properly. 96 | 97 | $level = Log::ERROR; 98 | if($e instanceof ErrorException && isset(self::$levels[$e->getSeverity()])) { 99 | $level = self::$levels[$e->getSeverity()]; 100 | } 101 | 102 | Log::getChannel('exception')->addRecord( 103 | $level, $log_message, array( 104 | 'exception' => $e 105 | ) 106 | ); 107 | 108 | $request = static::$request; 109 | if($request === null) 110 | $request = Request::createFromGlobals(); 111 | 112 | $action = Action::getDefaultAction(self::$_controller); 113 | $action->setRespondsWith(array('*'), false); 114 | $response = Controller::invoke($action, $request, array('exception' => $e)); 115 | $response->prepare($request); 116 | 117 | if($send_response) { 118 | $response->send(); 119 | } 120 | 121 | return $response; 122 | } catch(\Exception $_e) { 123 | $response = new Response(); 124 | $response->setStatusCode(500); 125 | 126 | if(Core::$_MODE === Core::MODE_PRODUCTION) { 127 | $response->setContent("Internal server error"); 128 | } else { 129 | $response->setContent( 130 | $e->__toString() . 131 | "\n\n\nAdditionally, the following exception occurred while trying to handle the error:\n\n" . 132 | $_e->__toString() 133 | ); 134 | } 135 | 136 | return $response; 137 | } 138 | } 139 | 140 | public function __construct($message, $code = null) { 141 | if($code == null && is_numeric($message)) { 142 | $code = intval($message); 143 | $message = $this->getInternalMessage($code); 144 | } 145 | parent::__construct($message, $code); 146 | } 147 | 148 | protected function getInternalMessage($type) { 149 | 150 | switch($type) { 151 | case Response::STATUS_NOT_FOUND : 152 | return 'The requested resource was not found'; 153 | case Response::STATUS_FORBIDDEN : 154 | return 'You do not have permission to access the requested resource'; 155 | case Response::STATUS_UNAUTHORISED : 156 | return 'Authorisation is required to complete this action'; 157 | case Response::STATUS_OK : 158 | return 'Request OK'; 159 | case Response::STATUS_BAD_REQUEST : 160 | return 'Bad request format or the format was not understood'; 161 | case Response::STATUS_CREATED : 162 | case Response::STATUS_ACCEPTED : 163 | case Response::STATUS_MOVED_PERMANENTLY : 164 | case Response::STATUS_NOT_MODIFIED : 165 | case Response::STATUS_MOVED_TEMPORARILY : 166 | case Response::STATUS_SERVER_ERROR : 167 | case Response::STATUS_NOT_IMPLEMENTED : 168 | default : 169 | return 'Unknown error'; 170 | } 171 | } 172 | 173 | public static function getResponseMethod() { 174 | if(self::$_response_method == null) { 175 | if(PHP_SAPI === 'cli') return Response::FORMAT_CLI; 176 | else return Config::get('wave')->response->default_format; 177 | } else 178 | return self::$_response_method; 179 | } 180 | 181 | public static function setRequest($request) { 182 | self::$request = $request; 183 | } 184 | 185 | public static function setResponse($response) { 186 | self::$response = $response; 187 | } 188 | } 189 | 190 | ?> -------------------------------------------------------------------------------- /src/Wave/Router/Action.php: -------------------------------------------------------------------------------- 1 | setAction($action_method); 39 | 40 | return $action; 41 | } 42 | 43 | public function __construct() 44 | { 45 | $this->setProfile('default'); 46 | $this->response_methods = (array)Config::get('wave')->router->base->methods; 47 | } 48 | 49 | public function addBaseRoute($baseroute) 50 | { 51 | if (substr($baseroute, -1, 1) !== '/') 52 | $baseroute .= '/'; 53 | 54 | $this->baseroutes[] = $baseroute; 55 | } 56 | 57 | public function getBaseRoutes() 58 | { 59 | if (empty($this->baseroutes)) { 60 | return array('/'); 61 | } else return $this->baseroutes; 62 | } 63 | 64 | public function addRoute($methods, $route) 65 | { 66 | 67 | // trim the last / off the end if necessary 68 | if (substr($route, -1, 1) == '/') 69 | $route = substr($route, 0, -1); 70 | 71 | if (array_search(Method::ANY, $methods) !== false) 72 | $methods = Method::$ALL; 73 | 74 | foreach ($this->getBaseRoutes() as $baseroute) { 75 | 76 | $new_route = $route; 77 | if (!isset($route[0]) || $route[0] !== '/') 78 | $new_route = $baseroute . $route; 79 | 80 | foreach ($methods as $method) { 81 | $this->routes[] = $method . $new_route; 82 | } 83 | } 84 | } 85 | 86 | public function getRoutes() 87 | { 88 | return $this->routes; 89 | } 90 | 91 | public function hasRoutes() 92 | { 93 | return isset($this->routes[0]); 94 | } 95 | 96 | public function setAction($action) 97 | { 98 | $this->target_action = $action; 99 | } 100 | 101 | public function getAction() 102 | { 103 | return $this->target_action; 104 | } 105 | 106 | public function setRequiresLevel(array $levels, $inherit) 107 | { 108 | $this->mergeArrays('requires_level', $levels, $inherit); 109 | return $this; 110 | } 111 | 112 | public function checkRequiredLevel(Request $request) 113 | { 114 | if (!empty($this->requires_level)) { 115 | $authorization = $request->getAuthorization(); 116 | if ($authorization instanceof Request\AuthorizationAware) 117 | return $authorization->hasAuthorization($this->requires_level, $request); 118 | elseif (is_callable($authorization)) 119 | return $authorization($this->requires_level, $request); 120 | else 121 | throw new UnauthorizedException("Unauthorized"); 122 | } else 123 | return true; 124 | 125 | } 126 | 127 | public function setRespondsWith(array $levels, $inherit) 128 | { 129 | $this->mergeArrays('response_methods', $levels, $inherit); 130 | return $this; 131 | } 132 | 133 | public function getRespondsWith() 134 | { 135 | return $this->response_methods; 136 | } 137 | 138 | public function canRespondWith($method) 139 | { 140 | foreach ($this->response_methods as $allowed) { 141 | if ($allowed === '*' || $allowed === $method) { 142 | return true; 143 | } 144 | } 145 | return false; 146 | } 147 | 148 | public function setProfile($profile) 149 | { 150 | $profiles = Config::get('deploy')->profiles; 151 | if (!isset($profiles->$profile)) { 152 | throw new Exception('BaseURL profile "' . $profile . '" is not defined in deploy configuration'); 153 | } else { 154 | $this->profile = $profile; 155 | $this->setBaseURL($profiles->$profile->baseurl); 156 | } 157 | } 158 | 159 | public function getProfile() 160 | { 161 | return $this->profile; 162 | } 163 | 164 | public function setBaseURL($baseurl) 165 | { 166 | $this->baseurl = $baseurl; 167 | } 168 | 169 | public function getBaseURL() 170 | { 171 | return $this->baseurl; 172 | } 173 | 174 | private function mergeArrays($property, array $items, $merge) 175 | { 176 | if ($merge) $this->$property = $items + $this->$property; 177 | else $this->$property = $items; 178 | } 179 | 180 | public function setValidationSchema($schema) 181 | { 182 | $this->validation_schema = $schema; 183 | } 184 | 185 | public function getValidationSchema(array $input_data = array()) 186 | { 187 | 188 | // check if any variables in the schema path need to be replaced 189 | if (strpos($this->validation_schema, '{') !== false) { 190 | $this->validation_schema = preg_replace_callback('/\{([0-9a-z_]+)\}/i', function ($matches) use ($input_data) { 191 | if (array_key_exists($matches[1], $input_data)) { 192 | return $input_data[$matches[1]]; 193 | } else return 'default'; 194 | }, $this->validation_schema); 195 | } 196 | 197 | return $this->validation_schema; 198 | } 199 | 200 | public function needsValidation() 201 | { 202 | return $this->validation_schema !== null; 203 | } 204 | 205 | public function addAnnotation(Annotation $annotation) 206 | { 207 | if (!array_key_exists($annotation->getKey(), $this->annotations)) 208 | $this->annotations[$annotation->getKey()] = array(); 209 | 210 | $this->annotations[$annotation->getKey()][] = $annotation; 211 | $annotation->apply($this); 212 | } 213 | 214 | /** 215 | * @return Annotation[] 216 | */ 217 | public function getAnnotations() 218 | { 219 | return $this->annotations; 220 | } 221 | 222 | /** 223 | * @param $key 224 | * @return Annotation|null 225 | */ 226 | public function getAnnotation($key) 227 | { 228 | if (isset($this->annotations[$key])) 229 | return $this->annotations[$key]; 230 | else 231 | return null; 232 | } 233 | 234 | public function hasAnnotation($key) 235 | { 236 | return isset($this->annotations[$key]); 237 | } 238 | 239 | public function addMethodParameter($name, $type) 240 | { 241 | $this->method_parameters[] = array($name, $type); 242 | } 243 | 244 | public function getMethodParameters() 245 | { 246 | return $this->method_parameters; 247 | } 248 | 249 | public function setSchedule($schedule_expression) 250 | { 251 | $this->schedule = $schedule_expression; 252 | } 253 | 254 | public function setScheduleTimezone($schedule_timezone) 255 | { 256 | $this->schedule_timezone = $schedule_timezone; 257 | } 258 | 259 | public function hasSchedule() 260 | { 261 | return $this->schedule; 262 | } 263 | 264 | public function hasScheduleTimezone() 265 | { 266 | return isset($this->schedule_timezone); 267 | } 268 | 269 | public function getSchedule() 270 | { 271 | return $this->schedule; 272 | } 273 | 274 | public function getScheduleTimezone() 275 | { 276 | return $this->schedule_timezone; 277 | } 278 | 279 | } 280 | 281 | 282 | ?> --------------------------------------------------------------------------------