diff --git a/.gitignore b/.gitignore index 57170a9e..5c5f19bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Created by .ignore support plugin (hsz.mobi) .idea +.vscode /vendor/ /cache/ /assets/index.twig diff --git a/Tina4/DataRecord.php b/Tina4/DataRecord.php index 532a8520..4363041a 100644 --- a/Tina4/DataRecord.php +++ b/Tina4/DataRecord.php @@ -7,15 +7,21 @@ * Notes: A record is a single part of the result set */ namespace Tina4; +use JsonSerializable; /** * Class DataRecord * @package Tina4 */ -class DataRecord +class DataRecord implements JsonSerializable { private $original; + /** + * This tests a string result from the DB to see if it is binary or not so it gets base64 encoded on the result + * @param $string + * @return bool + */ public function isBinary($string):bool { $isBinary=false; @@ -28,6 +34,42 @@ public function isBinary($string):bool return $isBinary; } + /** + * Gets a proper object name for returning back data + * @param string $name Improper object name + * @param array $fieldMapping Field mapping to map fields + * @return string Proper object name + */ + function getObjectName($name, $fieldMapping=[]) + { + if (!empty($fieldMapping) && $fieldMapping[$name]) { + return $fieldMapping[$name]; + } else { + $fieldName = ""; + if (strpos($name, "_") !== false) { + $name = strtolower($name); + for ($i = 0; $i < strlen($name); $i++) { + if ($name[$i] === "_") { + $i++; + $fieldName .= strtoupper($name[$i]); + } else { + $fieldName .= $name[$i]; + } + } + } else { + for ($i = 0; $i < strlen($name); $i++) { + if ($name[$i] !== strtolower($name[$i])) { + $fieldName .= "_" . strtolower($name[$i]); + } else { + $fieldName .= $name[$i]; + } + } + } + return $fieldName; + } + } + + /** * DataRecord constructor Converts array to object * @param array $record Array of records @@ -36,28 +78,40 @@ function __construct($record) { if (!empty($record)) { $this->original = (object)$record; - foreach ($record as $column => $value) { if ($this->isBinary($value)) { $value = \base64_encode($value); $this->original->{$column} = $value; } - $columnName = $column; - $this->$columnName = $value; - $columnName = strtoupper($column); - $this->$columnName = $value; + $this->{$column} = $value; } } + } + + /** + * Transform to a camel case result + */ + function transformObject () { + $object = (object)[]; + foreach ($this->original as $column => $value) { + $columnName = $this->getObjectName($column); + $object->$columnName = $value; + } - //print_r ($record); + return $object; } /** * Converts array to object + * @param bool $original Whether to get the result as original field names * @return object */ - function asObject () { - return $this->original; + function asObject ($original=false) { + if ($original) { + return $this->original; + } else { + return $this->transformObject(); + } } function asJSON () { @@ -75,4 +129,9 @@ function byName ($name) { return $this->$columnName; } } + + public function jsonSerialize() + { + return json_encode($this->original,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); + } } diff --git a/Tina4/DataResult.php b/Tina4/DataResult.php index d4e1ebed..a522ad1f 100644 --- a/Tina4/DataResult.php +++ b/Tina4/DataResult.php @@ -20,27 +20,27 @@ class DataResult implements JsonSerializable /** * @var resource Records returned from query */ - private $records; + public $records; /** * @var array Fields in the table and their types */ - private $fields; + public $fields; /** * @var integer Number of records */ - private $noOfRecords; + public $noOfRecords; /** * @var integer Data row offset */ - private $offSet; + public $offSet; /** * @var DataError Database error */ - private $error; + public $error; /** * DataResult constructor. @@ -92,24 +92,53 @@ function fields() /** * Converts returned results as array of objects + * @param boolean $original Original field name * @return array|null * @example examples\exampleDataResultRecords.php */ - function records() + function records($original=false) { $results = null; if (!empty($this->records)) { foreach ($this->records as $rid => $record) { - $results[] = $record->asObject(); + $results[] = $record->asObject($original); } } return $results; } + /** + * Gets an array of objects + * @param boolean $original Original field names + * @return array|mixed + */ + public function asObject($original=false) { + return $this->records($original); + } - + /** + * Gets an array of objects in the original form + * @return array|mixed + */ + public function asOriginal() { + return $this->records(true); + } + /** + * Gets the result as a generic array without the extra object information + * @param boolean $original Original field names + * @return array + */ + public function asArray($original=false) { + //$records = $this->jsonSerialize(); + $result = []; + foreach ($this->records() as $id => $record) { + $result[] = (array)$record; + } + return $result; + } + /** * Converts array of records to array of objects * @return false|string @@ -128,14 +157,10 @@ function __toString() } } - - - - if (!empty($results)) { - return json_encode((object)["recordsTotal" => $this->noOfRecords, "recordsFiltered" => $this->noOfRecords, "data" => $results, "error" => null]); + return json_encode((object)["recordsTotal" => $this->noOfRecords, "recordsFiltered" => $this->noOfRecords, "fields" => $this->fields, "data" => $results, "error" => null]); } else { - return json_encode((object)["recordsTotal" => 0, "recordsFiltered" => 0, "data" => [], "error" => $this->error->getErrorText()]); + return json_encode((object)["recordsTotal" => 0, "recordsFiltered" => 0, "fields" => [], "data" => [], "error" => $this->error->getErrorText()]); } } @@ -156,7 +181,7 @@ public function jsonSerialize() { } } - return (object)["recordsTotal" => $this->noOfRecords, "recordsFiltered" => $this->noOfRecords, "data" => $results, "error" => $this->getError()]; + return (object)["recordsTotal" => $this->noOfRecords, "recordsFiltered" => $this->noOfRecords, "fields" => $this->fields, "data" => $results, "error" => $this->getError()]; } /** diff --git a/Tina4/DataSQLite3.php b/Tina4/DataSQLite3.php index 0ee71858..a5677e64 100644 --- a/Tina4/DataSQLite3.php +++ b/Tina4/DataSQLite3.php @@ -62,7 +62,7 @@ public function native_exec() { } public function native_error() { - return (new DataError( $this->dbh->lastErrorCode(), $this->dbh->lastErrorMsg())); + return (new \Tina4\DataError( $this->dbh->lastErrorCode(), $this->dbh->lastErrorMsg())); } public function native_fetch($sql="", $noOfRecords=10, $offSet=0) { @@ -86,7 +86,6 @@ public function native_fetch($sql="", $noOfRecords=10, $offSet=0) { $fid = 0; $fields = []; foreach ($records[0] as $field => $value) { - $fields[] = (new DataField($fid, $recordCursor->columnName($fid), $recordCursor->columnName($fid), $recordCursor->columnType($fid))); $fid++; } @@ -98,8 +97,6 @@ public function native_fetch($sql="", $noOfRecords=10, $offSet=0) { $error = $this->error(); - - return (new DataResult($records, $fields, $countRecords, $offSet, $error)); } diff --git a/Tina4/DebugLog.php b/Tina4/DebugLog.php index 29c548af..610ec541 100644 --- a/Tina4/DebugLog.php +++ b/Tina4/DebugLog.php @@ -9,7 +9,7 @@ class DebugLog { static function message($message, $debugType=DEBUG_NONE){ - if($debugType == DEBUG_NONE) return; + if($debugType == DEBUG_NONE || TINA4_DEBUG === false) return; if (is_array($message)|| is_object($message)) { $message = print_r ($message, 1); diff --git a/Tina4/Routing.php b/Tina4/Routing.php index ab72df25..3c027a62 100644 --- a/Tina4/Routing.php +++ b/Tina4/Routing.php @@ -23,7 +23,7 @@ class Routing private $params; private $content; private $root; - private $subFolder; //Sub folder when stack runs under a directory + private $subFolder=""; //Sub folder when stack runs under a directory /** * @var string Type of method @@ -36,28 +36,137 @@ class Routing */ private $pathMatchExpression = "/([a-zA-Z0-9\\ \\! \\-\\}\\{\\.]*)\\//"; + function rangeDownload($file) { + + $fp = @fopen($file, 'rb'); + + $size = filesize($file); // File size + $length = $size; // Content length + $start = 0; // Start byte + $end = $size - 1; // End byte + // Now that we've gotten so far without errors we send the accept range header + /* At the moment we only support single ranges. + * Multiple ranges requires some more work to ensure it works correctly + * and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 + * + * Multirange support annouces itself with: + * header('Accept-Ranges: bytes'); + * + * Multirange content must be sent with multipart/byteranges mediatype, + * (mediatype = mimetype) + * as well as a boundry header to indicate the various chunks of data. + */ + header("Accept-Ranges: 0-$length"); + // header('Accept-Ranges: bytes'); + // multipart/byteranges + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 + if (isset($_SERVER['HTTP_RANGE'])) { + + $c_start = $start; + $c_end = $end; + // Extract the range string + list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2); + // Make sure the client hasn't sent us a multibyte range + if (strpos($range, ',') !== false) { + + // (?) Shoud this be issued here, or should the first + // range be used? Or should the header be ignored and + // we output the whole content? + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header("Content-Range: bytes $start-$end/$size"); + // (?) Echo some info to the client? + exit; + } + // If the range starts with an '-' we start from the beginning + // If not, we forward the file pointer + // And make sure to get the end byte if spesified + if ($range == '-') { + // The n-number of the last bytes is requested + $c_start = $size - substr($range, 1); + } + else { + + $range = explode('-', $range); + $c_start = $range[0]; + $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size; + } + /* Check the range and make sure it's treated according to the specs. + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + */ + // End bytes can not be larger than $end. + $c_end = ($c_end > $end) ? $end : $c_end; + // Validate the requested range and return an error if it's not correct. + if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) { + + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header("Content-Range: bytes $start-$end/$size"); + // (?) Echo some info to the client? + exit; + } + $start = $c_start; + $end = $c_end; + $length = $end - $start + 1; // Calculate new content length + fseek($fp, $start); + header('HTTP/1.1 206 Partial Content'); + } + // Notify the client the byte range we'll be outputting + header("Content-Range: bytes $start-$end/$size"); + header("Content-Length: $length"); + + // Start buffered download + $buffer = 1024 * 8; + while(!feof($fp) && ($p = ftell($fp)) <= $end) { + + if ($p + $buffer > $end) { + + // In case we're only outputtin a chunk, make sure we don't + // read past the length + $buffer = $end - $p + 1; + } + set_time_limit(0); // Reset time limit for big files + echo fread($fp, $buffer); + flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. + } + + fclose($fp); + + } + function returnStatic($fileName) { + $fileName = preg_replace('#/+#','/',$fileName); $ext = pathinfo($fileName, PATHINFO_EXTENSION); $mimeType = mime_content_type($fileName); + if ($ext === "mp4") { + $mimeType = "video/mp4"; + } + else if ($ext === "svg") { $mimeType = "image/svg+xml"; - } else - if ($ext === "css") { - $mimeType = "text/css"; - } + } else - if ($ext === "js") { - $mimeType = "application/javascript"; - } - header('Content-Type: ' . $mimeType); - header('Cache-Control: max-age=' . (60 * 60) . ', public'); - header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', time() + (60 * 60))); //1 hour expiry time + if ($ext === "css") { + $mimeType = "text/css"; + } + else + if ($ext === "js") { + $mimeType = "application/javascript"; + } - $fh = fopen($fileName, 'r'); - fpassthru($fh); - fclose($fh); + if (isset($_SERVER['HTTP_RANGE'])) { // do it for any device that supports byte-ranges not only iPhone + $this->rangeDownload($fileName); + exit; + } + else { + header('Content-Type: ' . $mimeType); + header('Cache-Control: max-age=' . (60 * 60) . ', public'); + header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', time() + (60 * 60))); //1 hour expiry time + + $fh = fopen($fileName, 'r'); + fpassthru($fh); + fclose($fh); + } exit; //we are done here, file will be delivered } @@ -78,19 +187,24 @@ function includeDirectory($dirName) { $d->close(); } + + + /** * Routing constructor. * @param string $root Where the document root is located + * @param string $subFolder Subfolder where the project sits * @param string $urlToParse URL being parsed * @param string $method Type of method e.g. ANY, POST, DELETE, etc * @param object $config Config containing configs from the initialization * @throws \ReflectionException */ - function __construct($root = "", $urlToParse = "", $method = "", $config=null) + function __construct($root = "", $subFolder="", $urlToParse = "", $method = "", $config=null) { if (!empty($root)) { $_SERVER["DOCUMENT_ROOT"] = $root; } + $this->root = $root; if (!empty($config) && isset($config->auth)) { @@ -99,9 +213,12 @@ function __construct($root = "", $urlToParse = "", $method = "", $config=null) $this->auth = new Auth($_SERVER["DOCUMENT_ROOT"], $urlToParse); } - $this->subFolder = str_replace ("index.php", "", $_SERVER["PHP_SELF"]); - if (!defined("TINA4_BASE_URL")) define ("TINA4_BASE_URL", substr($this->subFolder,0, -1)); - + if (!empty($subFolder)) { + $this->subFolder = $subFolder . "/"; + if (!defined("TINA4_BASE_URL")) define("TINA4_BASE_URL", $subFolder); + } else { + if (!defined("TINA4_BASE_URL")) define("TINA4_BASE_URL", ""); + } if (TINA4_DEBUG) { \Tina4\DebugLog::message("TINA4: URL to parse " . $urlToParse, TINA4_DEBUG_LEVEL); @@ -136,6 +253,7 @@ function __construct($root = "", $urlToParse = "", $method = "", $config=null) $urlToParse = $this->cleanURL($urlToParse); + if (!empty($this->subFolder)) { $urlToParse = str_replace($this->subFolder, "/", $urlToParse); } diff --git a/Tina4Php.php b/Tina4Php.php index e4d9f16b..16f9067c 100644 --- a/Tina4Php.php +++ b/Tina4Php.php @@ -11,6 +11,10 @@ use Phpfastcache\CacheManager; use Phpfastcache\Config\ConfigurationOption; +if(!defined("DEBUG_CONSOLE")) define("DEBUG_CONSOLE", 9001); +if(!defined("DEBUG_SCREEN"))define("DEBUG_SCREEN", 9002); +if(!defined("DEBUG_ALL"))define("DEBUG_ALL", 9003); +if(!defined("DEBUG_NONE")) define("DEBUG_NONE", 9004); /** * Class Tina4Php Main class used to set constants * @package Tina4 @@ -73,6 +77,33 @@ function gitInit($gitEnabled, $gitMessage, $push=false) { } } + /** + * Logic to determine the subfolder - result must be /folder/ + */ + function getSubFolder() { + + //Evaluate DOCUMENT_ROOT && + $documentRoot = ""; + if (isset($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $documentRoot = $_SERVER["CONTEXT_DOCUMENT_ROOT"]; + } else + if (isset($_SERVER["DOCUMENT_ROOT"])) { + $documentRoot = $_SERVER["DOCUMENT_ROOT"]; + } + $scriptName = $_SERVER["SCRIPT_FILENAME"]; + + + + //str_replace($documentRoot, "", $scriptName); + $subFolder = dirname( str_replace($documentRoot, "", $scriptName)); + + if ($subFolder === "/" || $subFolder === "." || (str_replace($documentRoot, "", $scriptName) === $_SERVER["SCRIPT_NAME"] && $_SERVER["SCRIPT_NAME"] === $_SERVER["REQUEST_URI"])) { + $subFolder = null; + + } + return $subFolder; + } + /** * Tina4Php constructor. * @param null $config @@ -133,6 +164,8 @@ function __construct($config = null) $this->documentRoot = TINA4_DOCUMENT_ROOT; } + + if (file_exists("Tina4Php.php")) { $this->documentRoot = realpath(dirname(__FILE__)); } @@ -154,15 +187,15 @@ function __construct($config = null) } if (!defined("TINA4_TEMPLATE_LOCATIONS")) { - define("TINA4_TEMPLATE_LOCATIONS", ["templates", "assets", "templates/snippets"]); + define("TINA4_TEMPLATE_LOCATIONS", ["src/templates", "src/assets", "src/templates/snippets"]); } if (!defined("TINA4_ROUTE_LOCATIONS")) { - define("TINA4_ROUTE_LOCATIONS", ["api", "routes"]); + define("TINA4_ROUTE_LOCATIONS", ["src/api", "src/routes"]); } if (!defined("TINA4_INCLUDE_LOCATIONS")) { - define("TINA4_INCLUDE_LOCATIONS", ["app", "objects"]); + define("TINA4_INCLUDE_LOCATIONS", ["src/app", "src/objects"]); } if (!defined("TINA4_ALLOW_ORIGINS")) { @@ -182,19 +215,15 @@ function __construct($config = null) global $arrRoutes; $arrRoutes = []; - $foldersToCopy = ["assets", "app", "api", "routes", "templates", "objects"]; + $foldersToCopy = ["assets", "app", "api", "routes", "templates", "objects", "bin"]; foreach ($foldersToCopy as $id => $folder) { //Check if folder is there - if (!file_exists($this->documentRoot . "/{$folder}") && !file_exists("Tina4Php.php")) { - \Tina4\Routing::recurseCopy($this->webRoot . "/{$folder}", $this->documentRoot . "/{$folder}"); + if (!file_exists($this->documentRoot . "/src/{$folder}") && !file_exists("Tina4Php.php")) { + \Tina4\Routing::recurseCopy($this->webRoot . "/{$folder}", $this->documentRoot . "/src/{$folder}"); } } - if (!file_exists($this->documentRoot ."/assets/index.twig") && file_exists($this->documentRoot ."/assets/documentation.twig")) { - file_put_contents("assets/index.twig", file_get_contents("assets/documentation.twig")); - } - //Add the .htaccess file for redirecting things if (!file_exists($this->documentRoot . "/.htaccess") && !file_exists("engine.php")) { copy($this->webRoot . "/.htaccess", $this->documentRoot . "/.htaccess"); @@ -330,9 +359,11 @@ function __construct($config = null) $twig->addExtension(new \Twig\Extension\DebugExtension()); $twig->addGlobal('Tina4', new \Tina4\Caller()); - $twig->addGlobal('baseUrl', substr(str_replace ("index.php", "", $_SERVER["PHP_SELF"]),0, -1)); - $twig->addGlobal('baseURL', substr(str_replace ("index.php", "", $_SERVER["PHP_SELF"]), 0, -1)); + $subFolder = $this->getSubFolder(); + $twig->addGlobal('baseUrl', $subFolder); + $twig->addGlobal('baseURL', $subFolder); $twig->addGlobal('uniqid', uniqid()); + } function iterateDirectory($path, $relativePath="") @@ -383,13 +414,13 @@ function __toString() //Delete the first instance of the alias in the REQUEST_URI $newRequestURI = stringReplaceFirst($_SERVER["CONTEXT_PREFIX"], "", $_SERVER["REQUEST_URI"]); - $string .= new \Tina4\Routing($this->documentRoot, $newRequestURI, $_SERVER["REQUEST_METHOD"], $this->config); + $string .= new \Tina4\Routing($this->documentRoot, $this->getSubFolder(), $newRequestURI, $_SERVER["REQUEST_METHOD"], $this->config); } else { - $string .= new \Tina4\Routing($this->documentRoot, $_SERVER["REQUEST_URI"], $_SERVER["REQUEST_METHOD"], $this->config); + $string .= new \Tina4\Routing($this->documentRoot, $this->getSubFolder(), $_SERVER["REQUEST_URI"], $_SERVER["REQUEST_METHOD"], $this->config); } } else { - $string .= new \Tina4\Routing($this->documentRoot, "/", "GET", $this->config); + $string .= new \Tina4\Routing($this->documentRoot, $this->getSubFolder(), "/", "GET", $this->config); } return $string; } @@ -513,4 +544,4 @@ function tina4_autoloader($class) } -spl_autoload_register('Tina4\tina4_autoloader'); +spl_autoload_register('Tina4\tina4_autoloader'); \ No newline at end of file diff --git a/assets/clouds.mp4 b/assets/clouds.mp4 new file mode 100644 index 00000000..992ebcd5 Binary files /dev/null and b/assets/clouds.mp4 differ diff --git a/assets/documentation.twig b/assets/documentation.twig index 208e09f5..a267fb57 100644 --- a/assets/documentation.twig +++ b/assets/documentation.twig @@ -4,86 +4,101 @@ Tina4- This Is Not Another Framework - + -
-

Tina4 - This Is Not Another 4ramework

- - -
- -
-
Installation
-
-

Installation

-

Install composer and run the following command from your project folder

-

+
+

Tina4 - This Is Not Another 4ramework

+ + +
+ +
+
Installation
+
+

Installation

+

Install composer and run the following command from your project folder

+

 composer require andrevanzuydam\tina4php
                 
-

index.php

Create an index.php file in your project folder after composer has installed the library, add the following code

-
<?php
+            

index.php

Create an index.php file in your project folder after composer has installed the library, add the following code

+
<?php
 require "vendor/autoload.php";
 echo new \Tina4\Tina4Php();
-

Run your application

-

+            

Run your application

+

 php -S localhost:7124 index.php
                 
-

Run with debugging (assumes you have xdebug enabled for PHP)

-

+            

Run with debugging (assumes you have xdebug enabled for PHP)

+

 XDEBUG_CONFIG="remote_host=127.0.0.1" php -S localhost:7124 index.php
                 
-
+
+ +
+ +
+
Routing & Swagger Annotations
+
+

Routing

+

Tina4 supports all the routing methods you should need to create an API or web site. Routes can be annotated to render Swagger documentation

+ +

+ Most important features +

    +
  • Inline Params - any params you embed in the route with {} - example /employees/{id}
  • +
  • \Tina4\Response object - An anonymous function which will take three params (html|object|array, http response code, content type)
  • +
  • \Tina4\Request object - An object which contains request parameters sent to the server
  • +
-
- -
-
Routing
-
-

Routing

-

Tina4 supports all the routing methods you should need to create an API or web site. Routes can be annotated to render Swagger documentation

-

In the next section we will look at a few examples

-
Hello World
-

Create a hello.php file in the routes or api folder, you can also name the file any name you want.

-
<?php
+
+            

+ +

In the next section we will look at a few examples

+
Hello World
+

Create a hello.php file in the routes or api folder, you can also name the file any name you want.

+
<?php
 Tina4\Get::add( "/hello-world", function (Tina4\Response $response) {
     return $response ("Hello World");
 });
                     
-

Browse to /hello-world end point to see your handy work

+

Browse to /hello-world end point to see your handy work

-
Hello Annotations
-

How about the same route with a parameter and then to test it we can use Swagger UI. The new code can simply be appended to the same file you just created

-
<?php
+            
Hello Annotations
+

How about the same route with a parameter and then to test it we can use Swagger UI. The new code can simply be appended to the same file you just created

+
<?php
 /**
  * Hello world end point with a parameter
  * @description Hello World Annotation
@@ -94,35 +109,35 @@ Tina4\Get::add( "/hello-world/{name}", function ($name, Tina4\Response $response
     return $response ("Hello World {$name}");
 });
                     
-

Browsing to the Swagger documentation should result in a UI where you can test your end point. Click on the "Try it out" button and input a param for name

-
+

Browsing to the Swagger documentation should result in a UI where you can test your end point. Click on the "Try it out" button and input a param for name

+
-
+
-
-
Templating
-
-

Templating

+
+
Templating
+
+

Templating

-
+
-
+
-
-
ORM
-
-

ORM

+
+
ORM
+
+

ORM

-
-
+
+ - + diff --git a/bin/tina4 b/bin/tina4 new file mode 100644 index 00000000..7c5a364a --- /dev/null +++ b/bin/tina4 @@ -0,0 +1,42 @@ +#!/usr/bin/env php +