<?php
namespace Fresh;
/**
* ## Component
* \Fresh\Component is the base class. It holds the views, a handler, and other settings that need to be global. It also processes form submissions before passing to your `submit` handler.
*
* @export(Contribute.Component)
*/
class Component {
use \Fresh\Readonly;
public $dir;
protected $name;
protected $views = [];
protected $handler;
private $r_handler;
protected $fileUploadDir;
protected $fileUploadUrlPrefix;
/**
* # Create a component
* `new \Fresh\Component($name,$dir);`
* I'm not actually sure what the name is used for, if anything.
* If you `extends \Fresh\Component`, you can declare `$name` and `$dir` as protected properties then one does not need to be supplied via the constructor
*
* @export(Class.Component)
*/
public function __construct($name = null,$dir = null){
$this->name = $name ?? $this->name;
$this->dir = $dir ?? $this->dir;
$this->handler = new Handler('runtime');
}
/**
* ## Uploading files
* To enable automatic file uploads, call `$component->setUpload($storageDir, $urlPrefix)`. You are responsible for routing that file.
* The `$urlPrefix` is used to prepend the file-url when passing formdata to your `submit` handler
*
* @export(Feature.uploadFile)
*/
public function setUpload($dir,$urlPrefix){
$this->fileUploadDir = $dir;
$this->fileUploadUrlPrefix = $urlPrefix;
}
public function viewNames(){
static $viewNames = null;
$viewDir = $this->dir.'/view/';
if ($viewNames===null){
$files = (new class{
function getFiles($path,$ext='*'){
$dh = opendir($path);
$files = [];
while ($file = readdir($dh)){
if ($file=='.'||$file=='..')continue;
$fPath = str_replace('//','/',$path.'/'.$file);
if (is_dir($fPath)){
$subFiles = $this->getFiles($fPath,$ext);
$files = array_merge($files,$subFiles);
} else if ($ext=='*'
||($fExt=pathinfo($fPath,PATHINFO_EXTENSION))==$ext){
$files[] = $fPath;
}
}
return $files;
}
})->getFiles($viewDir,'php');
foreach ($files as $file){
if (substr($file,-strlen('View.php'))==='View.php'){
$vn = substr($file,strlen($viewDir)-1,-4);
$viewNames[] = $vn;
}
}
// print_r($viewNames);
// exit;
return $viewNames;
} else {
return $viewNames;
}
}
/**
* # Load a view
* The `$passthru` will be [`extract`](https://www.php.net/manual/en/function.extract.php)ed so the the view file can access values directly by their keys.
* `$passthru` is optional.
*
* ```php
* $compo = new \Fresh\Component($componentName, $componentDir);
* $passthru = ['id'=>1];
* $viewName = 'Note';
* $view = $compo->view($viewName,$passthru);
* echo $view;
* ```
* This will load the file at `$componentDir/view/$viewNameView.php. To be clear, your view file must end in `View.php`
*
* @export(Feature.getView)
*/
public function view($view=null,$passthru=null){
if ($view==null)$view='View';
if (substr($view,-4)=='Form')$view = substr($view,0,-4);
if (substr($view,-4)=='View')$view = substr($view,0,-4);
$view .= 'View';
$file = $this->dir.'/view/'.$view.'.php';
if (!file_exists($file)){
return null;
}
$existing = $this->views[$file] ?? null;
if ($existing!=null){
if (is_array($passthru))$existing->setPassthru($passthru);
return $existing;
}
if ($passthru===null)$passthru = [];
if ($view=='View')$view = '';
$viewObj = new View($this,$view,$passthru);
$this->handler->addChild($viewObj);
$this->views[$file] = $viewObj;
return $viewObj;
}
/**
* # Load a form
* The `$passthru` will be [`extract`](https://www.php.net/manual/en/function.extract.php)ed so the the form file can access values directly by their keys.
* `$passthru` is optional.
*
* ```php
* $compo = new \Fresh\Component($componentName, $componentDir);
* $passthru = ['id'=>1, 'submit_url'=>'/submit/myform/'];
* $viewName = 'Note';
* $view = $compo->form($viewName,$passthru);
* echo $view;
* ```
* This will load the file at `$componentDir/view/$viewNameForm.php. To be clear, your form file must end in `Form.php`.
*
*
* @export(Feature.getForm)
*/
public function form($view='',$passthru=[]){
if ($view=='View')$view='';
else if (substr($view,-4)=='View')$view = substr($view,0,-4);
$view .= 'Form';
$file = $this->dir.'/view/'.$view.'.php';
if (!file_exists($file))return null;
$existing = $this->views[$file] ?? null;
if ($existing!=null){
$existing->setPassthru($passthru);
return $existing;
}
$view = substr($view,0,-strlen('Form'));
$formObj = new Form($this,$view,$passthru);
$this->handler->addChild($formObj);
$this->views[$file] = $formObj;
return $formObj;
}
/**
* ## Filter submitted data
* I haven't tested this, and I don't trust it. But here's my best notes on it.
*
* This uses a `filter` handler, which you set by:
* ```php
* $compo->setFilterHandler(
* function($name, $value, $input){
* //modify the value, like normalizing a phone number
* return $modifiedValue;
* }
* )
* ```
*
* I'm confused, and not 100% sure how this works in practice.
* Anyway, for a `$_POST['somekey'] = 'some value';`, there should be an `<input name="somekey" />` in the source form.
* Your `filter` handler will be called as `$filter('somekey','some value', $domInputNode)`.
* If there are multiple `filter` handlers... it might break.
*
* @export(Feature.filterSubmission)
*/
public function filter($name,$value,$input){
$filterererererer = $this->handler->compile->filter ?? null;
if ($filterererererer==null)return $value;
else {
$filteredededValue = $filterererererer($name,$value,$input);
return $filteredededValue;
}
}
/**
* ## Submitting data sort of automaticallyish
* You must still set a `submit` handler like so:
* ```php
* $myDbObj = get_my_db_instance();
* $compo->setSubmitHandler(
* function (string $tableName, array $dataToSave, $itemId) use ($myDbObj){
* $myDbObj->save($tableName,$dataToSave,$itemId);
* }
* );
* ```
*
* @export(Feature.submission)
*/
public function submit($formName,$passthru=[],$submitData=null){
//TODO how do file uploads work with this abstraction?
if ($submitData===null)$submitData = $_POST;
// if ($submitData===null)$submitData = $_GET;
$data = $submitData;
$form = $this->form($formName);
$inputs = $form->getInputs();
// echo 'this is where we submit... i guess.';
// echo implode("\n",$inputs);
$saveData = [];
foreach ($inputs as $input){
$name = $input->name;
if ($input->type=='checkbox'){
$value = ($submitData[$name]??false)=='on' ? 1 : 0;
$filteredValue = $this->filter($name,$value,$input);
$saveData[$name][] = $filteredValue;
unset($submitData[$name]);
} else if (isset($submitData[$name])){
$value = $submitData[$name];
$filteredValue = $this->filter($name,$value,$input);
$saveData[$name][] = $filteredValue;
unset($submitData[$name]);
} else if ($input->type=='file'){
$entry = $_FILES[$name] ?? null;
if ($entry==null)continue;
if ($this->fileUploadDir==null){
throw new \Exception("You must set File upload Info on your component to handle file uploads. Call \$component->setUpload(\$dir,\$urlPrefix)");
}
$permaFileName = $this->uploadFile($entry,$this->fileUploadDir);
if ($permaFileName!==false){
$saveData[$name][] = str_replace('//','/',$this->fileUploadUrlPrefix.'/'.$permaFileName);
}
// var_dump($saveData);
// exit;
}
}
$table = $submitData['fresh_table'] ?? null;
unset($submitData['fresh_table']);
$id = $submitData['id'] ?? null;
unset($submitData['id']);
if (count($submitData)>0){
throw new \Exception("There was more data submitted than the form allows.");
}
if ($id!==null)$saveData['id'][] = $id;
if ($table==null)throw new \Exception("Cannot autosubmit form because the table is null.");
$submitter = $this->handler->runtime->submit;
if ($submitter==null){
throw new \Exception("There is no submitter. Call setSubmitHandler(\$callback);... "
."where \$callback accepts: string \$tableName, array \$dataToSave, string|int \$itemId. "
."");
}
return $submitter($table,$saveData,$id,$passthru);
}
/**
* ## Routing, Setting things up
* Use the setup step to do routing, basically. This requires you to append setup code to views. Here's a simple (bad) example:
* ```php
* $phpCode = '<?php if ($url=='/')echo $this; ?>`
* //`$this` refers to the view which is being setup.
* $view = $compo->view("Page_Home"); //this points to $compoDir/view/Page_HomeView.php
* $view->appendSetupCode($phpCode);
* ```
* This setup code must be appended pre-compile or inside a compile handler.
*
* Then to execute your setup:
* ```php
* $compo = new \Fresh\Component($name,$componentDir);
* $forceRecompilation = true;// good when developing. Bad for production.
* $compo->compile($forceRecompilation); // Defaults to false, if you don't pass a paramater
* $passthru = ['url'=> cleanMyUrl( $_SERVER['REQUEST_URI'] ) ];
* $compo->setup($passthru);
* ```
* All your `setup` code will be run, in no particular order, and your `Page_Home` view will be displayed when `yoursite.atld/` is requested
*
* @export(Feature.setup)
*/
public function setup($passthru=[]){
$views = $this->viewNames();
foreach ($views as $name){
$v = $this->view($name);
$v->setup($passthru);
$f = $this->form($name) ?? null;
if ($f!=null)$f->setup($passthru);
}
}
/**
* ## Handling `rb-find` parameter.
* You can write `rb-find` in any way you want, but the built in idea is a simple list of `key1:value1;key2:value2;`
* call $compo->parseLookupString($find) to break those into an array.
*
* If your key or value contains a colon (:) or semi-colon (;), that'll probably break things.
*
* @export(Method.parseLookupStr)
*/
public function parseLookupStr(string $lookupStr): \stdClass {
$entries = explode(';',$lookupStr);
$out = [];
foreach ($entries as $e){
if ($e=='')continue;
$p = explode(':',$e);
$out[$p[0]] = $p[1];
}
return (object)$out;
}
/**
* ## Compiling
* Call `$compo->compile(true)` to force re-compilation of all views & forms. Call `$compo->compile()` to use the default `$forceRecompile` value of `false` to only compile views that have changed.
* You should use `true` in development probably.
* I've had troubles with conditional recompiling (passing `false`) on my localhost, and I think it was because of file permissions issues, but I'm not sure.
*
* And you can set a host of your own `compile` handlers.
*
* @export(Feature.compile)
*/
public function compile($forceRecompile=false){
$views = $this->viewNames();
foreach ($views as $name){
$v = $this->view($name);
$v->compile($forceRecompile);
$f = $this->form($name) ?? null;
if ($f!=null)$f->compile($forceRecompile);
}
}
/**
* See docs on the `filter()` method for more information
*/
public function setFilterHandler($callback){
$this->handler->setHandler('compile','filter',$callback);
}
/**
* ## Submission Handler
*
*
* @export(Handler.filter)
*/
public function setSubmitHandler($callback){
$this->handler->setHandler('runtime','submit',$callback);
}
/**
* ## Runtime Handlers
* Use `$compo->setRuntimeHandler(...)` or `$compo->addRuntimeHandler($handlerName,$function)`. What the handler receives varies greatly.
* `setRuntimeHandler` will only allow one handler per `$name` & the callback's value will be returned when the runtime handler is called.
* `addRuntimeHandler` will allow multiple handlers. When the named handler is called, each callback's value will be stored in an array & that array will be returned.
*
* ### Noteable runtime handlers:
* - `find`: `function($table, $findProperty){return $anArrayOfObjects};`
* - This MUST be declared to use the entity features
* - `format_formatterName`: `function($value){return convertToMyFormat($value);}`
* - `$compo->setRuntimeHandler('runtime','format_'.$formatterName,$callback);`
* - or `$compo->view($view)->addFormatter($name,$callback)`
* - `addResources`: `function($viewObj, $resourcesList){...use your framework or whatever...}
*
*
*
* @export(Extend.runtimeHandler)
*/
public function setRuntimeHandler($name,$callback){
$this->handler->setHandler('runtime',$name,$callback);
}
public function addRuntimeHandler($name,$callback){
$this->handler->addHandler('runtime',$name,$callback);
}
/**
* ## Compile Handlers
* These will be run during the compile step.
*
* NEED TO REDO THIS DOC FOR COMPILE HANDLERS
*
* Use `$compo->addCompileHandler($handlerName,$function)`. What the handler receives varies greatly.
*
* ### Noteable Compile handlers: (name: Callable example)
* - `preDoc`: `function($cleanSource,$viewContent,$compiler,$view){ return str_replace('<bad-tag', '<good-tag',$cleanSource); }`
* - This runs before the \Taeluf\PHTML (DOMDocument) instance is created
* - Generally, you should return a modified version of the $cleanSource
* - $cleanSource is the view's source code with the PHP extracted by $compiler
* - $viewContent is the view's source code, with inline PHP still in tact
* - $compiler is an instance of \Taeluf\PHTML\Compiler, which extracted the php
* - $view is an instance of the view object
* - `form`: `function($formViewObj,$doc,$compiler,$formNode){$formNode->setAttribute('action','/some/url/');}`
* - Return values are discarded, but you can modify the objects.
* - I'm not sure, but some things might be overwritten by this library, such as the `action` or `method` attributes on the node.
* - $formViewObj is an instance of \Fresh\Form
* - $doc is \Taeluf\PHTML instance
* - $compiler is an \Taeluf\PHTML\Compiler
* - $formNode is an \Taeluf\PHTML\Node (DOMElement) instance, who's tag is `<form ...>`
*
*
* @export(Extend.compileHandler)
*/
public function addCompileHandler($name,$callback){
$this->handler->addHandler('compile',$name,$callback);
}
/**
* ### View Queries: A compile handler
* In [my Liaison Package](https://github.com/Taeluf/Liaison-FreshCompo), I use this for routing. Here's a simplified version.
* ```php
* $compo->addViewQuery('//lia-route', //runs on every `<lia-route` tag
* function($rbDoc, $view, $compiler, $node){
* $route = $node->getAttribute('pattern');
* $escRoute = var_export($route, true); // because `$route` is only available during this compile step.
* $phpCode = '<?php if ($url==$escRoute)echo $this; ?>`
* $view->appendSetupCode($phpCode);
* }
* );
* ```
* This function will be run during the `compile` step for each `<lia-route` tag your view contains. No errors if not found.
* It will append setup code that does routing.
* Then when you call `$compo->setup['url'=>$theUrl]`, the setup code will run and your view will be displayed if the route matches the url.
*
* @export(Feature.viewQueries)
*/
public function addViewQuery($xpathQuery,$callback){
$this->handler->addHandler('query','all',['xpath'=>$xpathQuery,'callback'=>$callback]);
}
public function addHandler($domain,$name,$data){
$this->handlers->addHandler($domain,$name,$data);
}
public function uploadFile($file, $destinationFolder=null, $validExts = ['jpg', 'png','pdf'], $maxMB = 15)
{
if ($destinationFolder==null)$destinationFolder = $this->imageDestination;
if (!is_array($file) || $file == []
|| $file['size'] == 0
|| $file['name'] == ''
|| $file['tmp_name'] == ''
|| !is_int($file['error'])) {
return false;
}
try {
if (!isset($file['error']) ||
is_array($file['error'])
) {
throw new RuntimeException('Invalid parameters.');
}
switch ($file['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
throw new RuntimeException('No file sent.');
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
throw new RuntimeException('Exceeded filesize limit.');
default:
throw new RuntimeException('Unknown errors.');
}
// You should also check filesize here.
if ($file['size'] > ($maxMB * 1024 * 1024)) {
throw new RuntimeException('Exceeded filesize limit.');
}
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
if (!in_array($ext, $validExts)) {
// var_dump($ext);
// var_dump($validExts);
// var_dump($file);
// exit;
throw new RuntimeException('Invalid file format.');
}
if (!file_exists($destinationFolder)) {
mkdir($destinationFolder, 0771, true);
}
$fileName = sha1_file($file['tmp_name']) . '.' . $ext;
if (!move_uploaded_file(
$file['tmp_name'],
$destinationFolder . '/' . $fileName
)
) {
throw new RuntimeException('Failed to move uploaded file.');
}
return $fileName;
} catch (RuntimeException $e) {
throw $e;
}
}
}
// this could be useful
//
// public function submit($submitData){
// // var_dump($this->initData);
// // exit;
// // $this->insertHiddenIdInput($this->node);
// $fields = $this->getInputs();
// $table = $this->table;
// $filesToUpload = [];
// foreach ($fields as $index => $data) {
// $name = $data['name'];
// $required = $data['required'];
// $type = $data['type'];
// $tag = $data['tag'];
// $input = $data['input'];
// if ($type=='file'){
// $file = $_FILES[$name]?? null;
// if ($file==null)$pass=false;
// $filesToUpload[] = ['file'=>$_FILES[$name],
// 'inputData'=>$data
// ];
// unset($_FILES[$name]);
// if ($pass)continue;
// } else {
// $pass = $this->verify($data, $submitData[$name] ?? null);
// }
// if (!$pass) {
// throw new \Exception("Input '{$name}' failed verification");
// }
// $saveData[$name] = $submitData[$name];
// unset($submitData[$name]);
// }
// if (count($submitData) > 0) {
// // print_r($submitData);
// throw new \Exception("More data was sent to the server than was requested.");
// }
// foreach ($this->initData as $key=>$value){
// $saveData[$key] = $value;
// }
// foreach ($filesToUpload as $index=>$data){
// $file = $data['file'];
// $inputData = $data['inputData'];
// // print_r($this->options);
// // exit;
// $fileName = $this->schemaDoc->uploadFile($file);
// if ($fileName===false)continue;
// $urlPath = '/'.$this->schemaDoc->imageUrlPrefix.'/'.$fileName;
// $urlPath = str_replace(['///','//'],'/',$urlPath);
// $saveData[$inputData['name']] = $urlPath;
// }
// $bean = \RDB::dispense($table);
// $bean->import($saveData);
// \RDB::store($bean);
// }