datatable.directory codebase
				https://datatable.directory/
			
			
		
			You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							467 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
	
	
							467 lines
						
					
					
						
							12 KiB
						
					
					
				| <?php
 | |
| 
 | |
| 
 | |
| namespace App\Tables;
 | |
| 
 | |
| use App\Models\Revision;
 | |
| use App\Models\Row;
 | |
| use App\Models\Table;
 | |
| use Illuminate\Pagination\Paginator;
 | |
| use Illuminate\Queue\SerializesModels;
 | |
| use Illuminate\Support\Collection;
 | |
| use MightyPork\Exceptions\NotApplicableException;
 | |
| use MightyPork\Exceptions\NotExistException;
 | |
| use ReflectionClass;
 | |
| 
 | |
| /**
 | |
|  * Object representing a set of table modifications
 | |
|  */
 | |
| class Changeset
 | |
| {
 | |
|     use SerializesModels;
 | |
| 
 | |
|     const object_excluded_properties = [
 | |
|         'revision',
 | |
|         'table',
 | |
|         'note',
 | |
|     ];
 | |
| 
 | |
|     /**
 | |
|      * @var Revision - base revision this changeset belongs to
 | |
|      */
 | |
|     public $revision;
 | |
| 
 | |
|     /**
 | |
|      * @var Table - table this changeset belongs to
 | |
|      */
 | |
|     public $table;
 | |
| 
 | |
|     /**
 | |
|      * @var string - user's note attached to this changeset (future proposal)
 | |
|      */
 | |
|     public $note = '';
 | |
| 
 | |
|     /**
 | |
|      * Rows whose content changed, identified by _id.
 | |
|      * Only changed values are to be filled. Columns are identified by GCIDs
 | |
|      *
 | |
|      * Key'd by _id
 | |
|      *
 | |
|      * @var array|null - [_id -> [_id:…, cid:…, cid:…], …]
 | |
|      */
 | |
|     public $rowUpdates = [];
 | |
| 
 | |
|     /**
 | |
|      * New rows in the full Row::data format, including GRIDs.
 | |
|      * Values are identified by GCIDs from previously defined, or new columns.
 | |
|      *
 | |
|      * @var array|null - [_id -> [_id:…, cid:…, cid:…], …]
 | |
|      */
 | |
|     public $newRows = [];
 | |
| 
 | |
|     /**
 | |
|      * Rows to be removed
 | |
|      *
 | |
|      * @var int[]|null - GRIDs
 | |
|      */
 | |
|     public $removedRows = [];
 | |
| 
 | |
|     /**
 | |
|      * Values changed in column specifications, such as name, title, etc.
 | |
|      * This does not affect the table rows in any way.
 | |
|      *
 | |
|      * Key'd by id
 | |
|      *
 | |
|      * @var array[] - column specification objects, with GCIDs, key'd by CID
 | |
|      */
 | |
|     public $columnUpdates = [];
 | |
| 
 | |
|     /**
 | |
|      * New columns in the full format, including GCIDs
 | |
|      *
 | |
|      * @var array|null - [id -> [id:…, …], …]
 | |
|      */
 | |
|     public $newColumns = [];
 | |
| 
 | |
|     /**
 | |
|      * When reordering columns, here is the column IDs array
 | |
|      * in the new order. Columns meanwhile removed from the table
 | |
|      * or added to it are to be ignored or appended at the end.
 | |
|      *
 | |
|      * This shall not be filled if merely editing or adding columns,
 | |
|      * unless the order is explicitly adjusted by the user.
 | |
|      *
 | |
|      * @var string[]|null - GCIDs
 | |
|      */
 | |
|     public $columnOrder = [];
 | |
| 
 | |
|     /**
 | |
|      * Columns to be removed
 | |
|      *
 | |
|      * The data associated to those columns may or may not be removed from the Rows.
 | |
|      * It does not matter, other than in regard to the table size.
 | |
|      *
 | |
|      * @var int[]|null - GCIDs
 | |
|      */
 | |
|     public $removedColumns = [];
 | |
| 
 | |
|     private function walkProps()
 | |
|     {
 | |
|         $properties = (new ReflectionClass($this))->getProperties();
 | |
| 
 | |
|         foreach ($properties as $property) {
 | |
|             if (in_array($property->name, self::object_excluded_properties)) {
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             yield $property->name;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Reconstruct from a object, such as one found in Proposal.
 | |
|      * Note that the fromProposal() method should be used when the
 | |
|      * proposal is available, as it populates additional fields.
 | |
|      *
 | |
|      * @param \stdClass $changes
 | |
|      * @return Changeset
 | |
|      */
 | |
|     public static function fromObject($changes)
 | |
|     {
 | |
|         $changeset = new Changeset();
 | |
| 
 | |
|         foreach ($changeset->walkProps() as $prop) {
 | |
|             if (isset($changes->$prop)) {
 | |
|                 $changeset->$prop = $changes->$prop;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return $changeset;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Serialize to an object format that may be stored in a Proposal.
 | |
|      *
 | |
|      * @return \stdClass
 | |
|      */
 | |
|     public function toObject()
 | |
|     {
 | |
|         $object = new \stdClass();
 | |
| 
 | |
|         foreach ($this->walkProps() as $prop) {
 | |
|             $object->$prop = $this->$prop;
 | |
|         }
 | |
| 
 | |
|         return $object;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Check if there is any change in this changeset
 | |
|      *
 | |
|      * @return bool - any found
 | |
|      */
 | |
|     public function hasAnyChanges()
 | |
|     {
 | |
|         foreach ($this->walkProps() as $prop) {
 | |
|             if (!empty($this->$prop)) {
 | |
|                 return true;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Decorate / transform a single row for the editor view
 | |
|      *
 | |
|      * @param object|\DecoratedRow $row - row, must be key'd by column ids
 | |
|      * @param bool $decorate - to add extra underscored info for the editor
 | |
|      * @return \DecoratedRow|object|null - null if not decorating and the row was removed
 | |
|      */
 | |
|     public function transformRow($row, $decorate)
 | |
|     {
 | |
|         if ($row instanceof Row) $row = (object)$row->getAttributes();
 | |
| 
 | |
|         if ($decorate) {
 | |
|             $row->_remove = false;
 | |
|             $row->_changed = [];
 | |
|             $row->_orig = [];
 | |
|         }
 | |
| 
 | |
|         if ($this->isNewRow($row->_id)) {
 | |
|             if ($decorate) {
 | |
|                 $row->_new = true;
 | |
|             }
 | |
|             return $row;
 | |
|         }
 | |
| 
 | |
|         // Removed rows - return as null
 | |
|         if (in_array($row->_id, $this->removedRows)) {
 | |
|             if ($decorate) {
 | |
|                 $row->_remove = true;
 | |
|             } else {
 | |
|                 return null;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // if marked for removal, hide changes
 | |
|         if (!$row->_remove) {
 | |
|             // Changed values
 | |
|             if (isset($this->rowUpdates[$row->_id])) {
 | |
|                 $newVals = $this->rowUpdates[$row->_id];
 | |
| 
 | |
|                 if ($decorate) {
 | |
|                     $row->_changed = array_keys($newVals);
 | |
|                     $row->_orig = array_only((array)$row, $row->_changed);
 | |
|                 }
 | |
| 
 | |
|                 $row = (object)array_merge((array)$row, $newVals);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Drop deleted columns
 | |
|         if (!$decorate) {
 | |
|             foreach ($this->removedColumns as $colId) {
 | |
|                 unset($row->$colId);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         unset($row->_row_pivot);
 | |
| 
 | |
|         return $row;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Decorate / transform columns (loaded from the source revision)
 | |
|      *
 | |
|      * @return Column[]
 | |
|      */
 | |
|     public function fetchAndTransformColumns()
 | |
|     {
 | |
|         /** @var Column[] - loaded and transformed columns, cached from previous call to transformColumns() */
 | |
|         static $cachedColumns = [];
 | |
| 
 | |
|         if ($cachedColumns) return $cachedColumns;
 | |
|         $columns = Column::columnsFromJson($this->revision->columns);
 | |
| 
 | |
|         // Modify columns
 | |
|         foreach ($columns as $column) {
 | |
|             if (isset($this->columnUpdates[$column->id])) {
 | |
|                 $column->modifyByChangeset($this->columnUpdates[$column->id]);
 | |
|             }
 | |
| 
 | |
|             if (in_array($column->id, $this->removedColumns)) {
 | |
|                 $column->markForRemoval();
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Append new columns
 | |
|         foreach ($this->newColumns as $newColumn) {
 | |
|             $columns[] = $c = new Column($newColumn);
 | |
|             $c->markAsNew();
 | |
|         }
 | |
| 
 | |
|         // Reorder
 | |
|         $colsById = collect($columns)->keyBy('id')->all();
 | |
|         $newOrder = [];
 | |
|         foreach ($this->columnOrder as $id) {
 | |
|             $newOrder[] = $colsById[$id];
 | |
|         }
 | |
| 
 | |
|         $leftover_keys = array_diff(array_keys($colsById), $this->columnOrder);
 | |
|         foreach ($leftover_keys as $id) {
 | |
|             $newOrder[] = $colsById[$id];
 | |
|         }
 | |
| 
 | |
|         return $cachedColumns = $newOrder;
 | |
|     }
 | |
| 
 | |
|     public function rowRemove(int $id)
 | |
|     {
 | |
|         if ($this->isNewRow($id)) {
 | |
|             unset($this->newRows[$id]);
 | |
|         }
 | |
|         else {
 | |
|             $this->removedRows[] = $id;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public function rowRestore(int $id)
 | |
|     {
 | |
|         $this->removedRows = array_diff($this->removedRows, [$id]);
 | |
|     }
 | |
| 
 | |
|     public function isNewRow(int $id)
 | |
|     {
 | |
|         return isset($this->newRows[$id]);
 | |
|     }
 | |
| 
 | |
|     public function fetchAndTransformRow(int $id)
 | |
|     {
 | |
|         $r = $this->fetchRow($id);
 | |
|         $transformed = $this->transformRow($r, true);
 | |
|         return $transformed;
 | |
|     }
 | |
| 
 | |
|     public function fetchColumns()
 | |
|     {
 | |
|         return Column::columnsFromJson($this->revision->columns);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Fetch an existing row from DB, or a new row.
 | |
|      *
 | |
|      * @param $id
 | |
|      * @return object
 | |
|      */
 | |
|     public function fetchRow(int $id)
 | |
|     {
 | |
|         if ($this->isNewRow($id)) {
 | |
|             return (object)$this->newRows[$id];
 | |
|         }
 | |
| 
 | |
|         $r = $this->revision->rowsData($this->fetchColumns(), true, false)
 | |
|             ->whereRaw("data->>'_id' = ?", $id)->first();
 | |
| 
 | |
|         if (!$r) throw new NotExistException("No such row _id = $id in this revision.");
 | |
| 
 | |
|         // remove junk
 | |
|         unset($r->pivot_revision_id);
 | |
|         unset($r->pivot_row_id);
 | |
| 
 | |
|         return (object)$r->getAttributes();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Apply a row update, adding the row to the list of changes, or removing it
 | |
|      * if all differences were undone.
 | |
|      *
 | |
|      * @param array|object $newVals - values of the new row
 | |
|      */
 | |
|     public function rowUpdate($newVals)
 | |
|     {
 | |
|         $newVals = (object)$newVals;
 | |
| 
 | |
|         $_id = $newVals->_id;
 | |
|         $origRow = $this->fetchRow($_id);
 | |
| 
 | |
|         /** @var Column[]|Collection $cols */
 | |
|         $cols = collect($this->fetchAndTransformColumns())->keyBy('id');
 | |
| 
 | |
|         $updateObj = [];
 | |
| 
 | |
|         foreach ($newVals as $colId => $value) {
 | |
|             if (starts_with($colId, '_')) continue; // internals
 | |
| 
 | |
|             $col = $cols[$colId];
 | |
|             $value = $col->cast($value);
 | |
|             $origValue = $col->cast(isset($origRow->$colId) ? $origRow->$colId : null);
 | |
| 
 | |
|             if ($value !== $origValue) {
 | |
|                 $updateObj[$colId] = $value;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if ($this->isNewRow($_id)) {
 | |
|             $this->newRows[$_id] = array_merge($this->newRows[$_id], $updateObj);
 | |
|         }
 | |
|         else {
 | |
|             if (!empty($updateObj)) {
 | |
|                 $this->rowUpdates[$_id] = $updateObj;
 | |
|             } else {
 | |
|                 // remove possible old update record for this row, if nothing changes now
 | |
|                 unset($this->rowUpdates[$_id]);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get a page of added rows for display in the editor
 | |
|      *
 | |
|      * @param int $perPage
 | |
|      * @return \Illuminate\Pagination\LengthAwarePaginator|Collection|array
 | |
|      */
 | |
|     public function getAddedRows($perPage = 25)
 | |
|     {
 | |
|         return collection_paginate($this->newRows, $perPage);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param Column[] $columns
 | |
|      * @param array $csvArray
 | |
|      * @param bool $forTableInsert
 | |
|      * @return \array[][]|Collection
 | |
|      */
 | |
|     public static function csvToRowsArray($columns, $csvArray, $forTableInsert)
 | |
|     {
 | |
|         /** @var Collection|array[][] $rows */
 | |
|         $rows = collect($csvArray)->map(function ($row) use ($columns, $forTableInsert) {
 | |
|             if (count($row) == 0 || count($row) == 1 && $row[0] == '') return null; // discard empty lines
 | |
|             if (count($row) != count($columns)) {
 | |
|                 throw new NotApplicableException("All rows must have " . count($columns) . " fields.");
 | |
|             }
 | |
| 
 | |
|             $data = [];
 | |
| 
 | |
|             foreach ($row as $i => $val) {
 | |
|                 $col = $columns[$i];
 | |
| 
 | |
|                 if (strlen($val) > 1000) {
 | |
|                     // try to stop people inserting unstructured crap / malformed CSV
 | |
|                     throw new NotApplicableException("Value for column {$col->name} too long.");
 | |
|                 }
 | |
|                 $data[$col->id] = $col->cast($val);
 | |
|             }
 | |
| 
 | |
|             if ($forTableInsert) {
 | |
|                 return ['data' => $data];
 | |
|             } else {
 | |
|                 return $data;
 | |
|             }
 | |
|         })->filter();
 | |
| 
 | |
| 
 | |
|         $rowNumerator = new RowNumerator(count($csvArray));
 | |
| 
 | |
|         if ($forTableInsert) {
 | |
|             return $rows->map(function ($row) use (&$rowNumerator) {
 | |
|                 $row['data']['_id'] = $rowNumerator->next();
 | |
|                 return $row;
 | |
|             });
 | |
|         }
 | |
|         else {
 | |
|             return $rows->map(function ($row) use (&$rowNumerator) {
 | |
|                 $row['_id'] = $rowNumerator->next();
 | |
|                 return $row;
 | |
|             });
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public function addBlankRows(int $count)
 | |
|     {
 | |
|         $numerator = new RowNumerator($count);
 | |
| 
 | |
|         $columns = $this->fetchAndTransformColumns();
 | |
|         $template = [];
 | |
|         foreach ($columns as $column) {
 | |
|             $template[$column->id] = $column->cast(null);
 | |
|         }
 | |
| 
 | |
|         foreach ($numerator->generate() as $_id) {
 | |
|             $row = $template;
 | |
|             $row['_id'] = $_id;
 | |
|             $this->newRows[$_id] = $row;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public function addFilledRows($csvArray)
 | |
|     {
 | |
|         /** @var Column[] $columns */
 | |
|         $columns = array_values($this->fetchAndTransformColumns());
 | |
| 
 | |
|         $rows = self::csvToRowsArray($columns, $csvArray, false)
 | |
|             ->keyBy('_id');
 | |
| 
 | |
|         $this->newRows = array_merge($this->newRows, $rows->all());
 | |
|     }
 | |
| }
 | |
| 
 |