├── .gitmodules ├── phpunit.xml ├── test ├── lib │ ├── bootstrap.php │ └── testcase.php ├── LibraryTest.php ├── QueryTest.php └── ItemTest.php ├── bootstrap.php ├── library ├── query.php ├── library.php └── item.php └── readme.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "toolkit"] 2 | path = toolkit 3 | url = https://github.com/getkirby/toolkit.git 4 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | test 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/lib/bootstrap.php: -------------------------------------------------------------------------------- 1 | __DIR__ . DS . 'library' . DS . 'library.php', 11 | 'library\\item' => __DIR__ . DS . 'library' . DS . 'item.php', 12 | 'library\\query' => __DIR__ . DS . 'library' . DS . 'query.php', 13 | )); -------------------------------------------------------------------------------- /test/lib/testcase.php: -------------------------------------------------------------------------------- 1 | root); 12 | 13 | // set up a new library 14 | $this->library(); 15 | 16 | } 17 | 18 | protected function tearDown() { 19 | dir::remove($this->root); 20 | } 21 | 22 | public function library() { 23 | return $this->library = new Library($this->root); 24 | } 25 | 26 | public function item() { 27 | return new Library\Item($this->library, 'test'); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /test/LibraryTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($this->library->root(), $this->root); 10 | } 11 | 12 | public function testIsWritable() { 13 | $this->assertTrue($this->library->isWritable()); 14 | } 15 | 16 | public function testDatabase() { 17 | $this->assertInstanceOf('Database', $this->library->database()); 18 | } 19 | 20 | public function testIndex() { 21 | $this->assertInstanceOf('Database\\Query', $this->library->index()); 22 | } 23 | 24 | public function testFolder() { 25 | 26 | // test the folder object 27 | $this->assertInstanceOf('Folder', $this->library->folder()); 28 | $this->assertEquals($this->library->folder()->root(), $this->library->root()); 29 | 30 | } 31 | 32 | public function testId() { 33 | 34 | $this->assertTrue(is_string($this->library->id())); 35 | $this->assertTrue(strlen($this->library->id()) == 32); 36 | 37 | } 38 | 39 | public function testColumns() { 40 | 41 | $columns = array( 42 | 'id', 43 | 'type', 44 | 'status', 45 | 'created', 46 | 'updated' 47 | ); 48 | 49 | $this->assertEquals($this->library->columns()->pluck('name'), $columns); 50 | 51 | } 52 | 53 | public function testCreate() { 54 | 55 | $item = $this->library->create('todo', array( 56 | 'text' => 'Test this thing' 57 | )); 58 | 59 | $this->assertInstanceOf('Library\\Item', $item); 60 | 61 | } 62 | 63 | public function testItem() { 64 | 65 | $item = $this->library->item(array( 66 | 'id' => $this->library->id(), 67 | 'type' => 'test', 68 | 'status' => 'draft', 69 | 'updated' => time(), 70 | 'created' => time(), 71 | )); 72 | 73 | $this->assertInstanceOf('Library\\Item', $item); 74 | 75 | } 76 | 77 | public function testDelete() { 78 | 79 | $item = $this->library->create('todo'); 80 | $id = $item->id(); 81 | 82 | // check if the item can be found 83 | $this->assertInstanceOf('Library\\Item', $this->library->find($id)); 84 | 85 | // now delete it 86 | $item->delete(); 87 | 88 | // and check again 89 | $this->assertFalse($this->library->find($id)); 90 | 91 | } 92 | 93 | public function testRebuild() { 94 | 95 | $a = $this->library->create('test'); 96 | $b = $this->library->create('test'); 97 | 98 | // delete the index 99 | f::remove($this->library->root() . DS . 'library.sqlite'); 100 | 101 | $this->library->rebuild(); 102 | 103 | $this->assertEquals(2, $this->library->count()); 104 | $this->assertInstanceOf('Library\\Item', $this->library->find($b->id())); 105 | 106 | } 107 | 108 | } -------------------------------------------------------------------------------- /test/QueryTest.php: -------------------------------------------------------------------------------- 1 | library->create('todo', array('text' => 'first todo')); 11 | $this->library->create('todo', array('text' => 'second todo', 'status' => 'public')); 12 | 13 | // create some articles 14 | $this->library->create('article', array('text' => 'first article', 'created' => strtotime('2012-12-12 22:33'))); 15 | $this->library->create('article', array('text' => 'second article', 'status' => 'public')); 16 | $this->library->create('article', array('text' => 'third article', 'status' => 'private')); 17 | 18 | } 19 | 20 | public function testType() { 21 | 22 | $this->dummyData(); 23 | 24 | $this->assertEquals(2, $this->library->type('todo')->count()); 25 | $this->assertEquals(3, $this->library->type('article')->count()); 26 | 27 | } 28 | 29 | public function testStatus() { 30 | 31 | $this->dummyData(); 32 | 33 | $this->assertEquals(2, $this->library->status('draft')->count()); 34 | $this->assertEquals(2, $this->library->status('public')->count()); 35 | $this->assertEquals(1, $this->library->status('private')->count()); 36 | 37 | } 38 | 39 | public function testYear() { 40 | 41 | $this->dummyData(); 42 | 43 | $this->assertEquals(1, $this->library->year(2012)->count()); 44 | $this->assertEquals(4, $this->library->year(date('Y'))->count()); 45 | 46 | } 47 | 48 | public function testMonth() { 49 | 50 | $this->dummyData(); 51 | 52 | $this->assertEquals(1, $this->library->month('2012-12')->count()); 53 | $this->assertEquals(4, $this->library->month(date('Y-m'))->count()); 54 | 55 | } 56 | 57 | public function testDay() { 58 | 59 | $this->dummyData(); 60 | 61 | $this->assertEquals(1, $this->library->day('2012-12-12')->count()); 62 | $this->assertEquals(4, $this->library->day(date('Y-m-d'))->count()); 63 | 64 | } 65 | 66 | public function testSearch() { 67 | 68 | $this->dummyData(); 69 | $this->assertEquals(3, $this->library->search('article')->count()); 70 | 71 | } 72 | 73 | public function testCount() { 74 | 75 | $this->dummyData(); 76 | $this->assertEquals(5, $this->library->count()); 77 | 78 | } 79 | 80 | public function testYears() { 81 | 82 | $this->dummyData(); 83 | $this->assertEquals($this->library->years(), array(2012, date('Y'))); 84 | 85 | } 86 | 87 | public function testMonths() { 88 | 89 | $this->dummyData(); 90 | $this->assertEquals($this->library->months(), array('2012-12', date('Y-m'))); 91 | 92 | } 93 | 94 | public function testDays() { 95 | 96 | $this->dummyData(); 97 | $this->assertEquals($this->library->days(), array('2012-12-12', date('Y-m-d'))); 98 | 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /library/query.php: -------------------------------------------------------------------------------- 1 | library = $library; 14 | parent::__construct($library->database, 'items'); 15 | } 16 | 17 | protected function query($query, $params = array()) { 18 | 19 | $result = parent::query($query, $params); 20 | 21 | if(is_a($result, 'Collection')) { 22 | $library = $this->library; 23 | $result = $result->map(function($item) use($library) { 24 | return $library->item((array)$item); 25 | }); 26 | return $result; 27 | } else if(is_a($result, 'Obj')) { 28 | 29 | if(isset($result->aggregation)) { 30 | return $result; 31 | } else { 32 | return $this->library->item((array)$result); 33 | } 34 | 35 | } else { 36 | return $result; 37 | } 38 | 39 | } 40 | 41 | public function find($id) { 42 | return $this->where('id', '=', $id)->one(); 43 | } 44 | 45 | public function type($type) { 46 | return $this->where('type', '=', $type); 47 | } 48 | 49 | public function day($day) { 50 | 51 | if(!preg_match('!^[\d]{4}-[\d]{2}-[\d]{2}$!', $day)) { 52 | throw new Exception('Invalid day format. Must be YYYY-MM-DD'); 53 | } 54 | 55 | $start = strtotime($day); 56 | $end = strtotime('+1 day', $start); 57 | return $this->where('created', '>=', $start)->where('created', '<', $end); 58 | 59 | } 60 | 61 | public function month($month) { 62 | 63 | if(!preg_match('!^[\d]{4}-[\d]{2}$!', $month)) { 64 | throw new Exception('Invalid month format. Must be YYYY-MM'); 65 | } 66 | 67 | $start = strtotime($month . '-01'); 68 | $end = strtotime('+1 month', $start); 69 | return $this->where('created', '>=', $start)->where('created', '<', $end); 70 | 71 | } 72 | 73 | public function year($year) { 74 | 75 | if(!preg_match('!^[\d]{4}$!', $year)) { 76 | throw new Exception('Invalid year. Must be YYYY'); 77 | } 78 | 79 | $start = strtotime($year . '-01-01'); 80 | $end = strtotime('+1 year', $start); 81 | return $this->where('created', '>=', $start)->where('created', '<', $end); 82 | } 83 | 84 | public function years() { 85 | return $this->library->database->query('select distinct strftime("%Y", created, "unixepoch") as year from items')->pluck('year'); 86 | } 87 | 88 | public function months() { 89 | return $this->library->database->query('select distinct strftime("%Y-%m", created, "unixepoch") as month from items')->pluck('month'); 90 | } 91 | 92 | public function days() { 93 | return $this->library->database->query('select distinct strftime("%Y-%m-%d", created, "unixepoch") as day from items')->pluck('day'); 94 | } 95 | 96 | public function status($status) { 97 | return $this->where('status', '=', $status); 98 | } 99 | 100 | public function delete($where = null) { 101 | foreach($this->all() as $item) { 102 | $item->delete(); 103 | } 104 | } 105 | 106 | public function search($q, $columns = array()) { 107 | 108 | $query = $this; 109 | 110 | if(empty($columns)) { 111 | $columns = $this->library->columns()->pluck('name'); 112 | $columns = array_diff($columns, Item::$required); 113 | } 114 | 115 | $query->where(function($where) use($q, $columns) { 116 | foreach($columns as $column) { 117 | $where->orWhere($column, 'LIKE', '%' . $q . '%'); 118 | } 119 | return $where; 120 | }); 121 | 122 | return $query; 123 | 124 | } 125 | 126 | } -------------------------------------------------------------------------------- /library/library.php: -------------------------------------------------------------------------------- 1 | root = $root; 16 | 17 | if(!$this->isWritable()) { 18 | throw new Exception('The library is not writable'); 19 | } 20 | 21 | $this->index(); 22 | 23 | } 24 | 25 | public function root() { 26 | return $this->root; 27 | } 28 | 29 | public function columns() { 30 | return $this->database->query('PRAGMA table_info(items)'); 31 | } 32 | 33 | public function folder() { 34 | return new Folder($this->root); 35 | } 36 | 37 | public function isWritable() { 38 | return is_writable($this->root); 39 | } 40 | 41 | public function index() { 42 | 43 | if(!is_null($this->index)) return $this->index; 44 | 45 | $file = $this->root . DS . 'library.sqlite'; 46 | $setup = !file_exists($file); 47 | 48 | $this->database = new Database(array( 49 | 'database' => $file, 50 | 'type' => 'sqlite' 51 | )); 52 | 53 | if($setup) { 54 | $this->setup(); 55 | } 56 | 57 | return $this->index = new Query($this); 58 | 59 | } 60 | 61 | public function database() { 62 | return $this->database; 63 | } 64 | 65 | public function id() { 66 | 67 | $id = str::random(32); 68 | 69 | while($this->index()->where('id', '=', $id)->one()) { 70 | $id = str::random(32); 71 | } 72 | 73 | return $id; 74 | 75 | } 76 | 77 | public function create($type, $data = array()) { 78 | return Item::create($this, $type, $data); 79 | } 80 | 81 | public function item($data) { 82 | return new Item($this, $data['type'], $data); 83 | } 84 | 85 | public function delete($id) { 86 | if($item = $this->find($id)) { 87 | return $item->delete(); 88 | } else { 89 | throw new Exception('The item could not be found'); 90 | } 91 | } 92 | 93 | public function __call($method, $args) { 94 | 95 | // allowed query calls 96 | $queries = array( 97 | 'status', 98 | 'type', 99 | 'find', 100 | 'first', 101 | 'all', 102 | 'page', 103 | 'search', 104 | 'where', 105 | 'count', 106 | 'year', 107 | 'month', 108 | 'day', 109 | 'years', 110 | 'months', 111 | 'days', 112 | 'order' 113 | ); 114 | 115 | if(in_array($method, $queries)) { 116 | return call(array($this->index, $method), $args); 117 | } else { 118 | throw new Exception('Invalid library method'); 119 | } 120 | 121 | } 122 | 123 | public function setup() { 124 | 125 | $this->database->createTable('items', array( 126 | 'id' => array( 127 | 'type' => 'text', 128 | 'key' => 'unique', 129 | ), 130 | 'type' => array( 131 | 'type' => 'text', 132 | 'key' => 'index', 133 | ), 134 | 'status' => array( 135 | 'type' => 'text', 136 | 'key' => 'index', 137 | ), 138 | 'created' => array( 139 | 'type' => 'timestamp', 140 | 'key' => 'index', 141 | ), 142 | 'updated' => array( 143 | 'type' => 'timestamp' 144 | ) 145 | )); 146 | 147 | $this->rebuild(); 148 | 149 | } 150 | 151 | public function rebuild() { 152 | 153 | foreach($this->folder()->children() as $year) { 154 | foreach($year->children() as $month) { 155 | foreach($month->children() as $day) { 156 | foreach($day->children() as $item) { 157 | $data = data::read($item->root() . DS . 'item.yaml'); 158 | $item = new Item($this, $data['type'], $data); 159 | $item->store(); 160 | } 161 | } 162 | } 163 | } 164 | 165 | } 166 | 167 | public function clean($root) { 168 | 169 | if(!is_dir($root)) { 170 | throw new Exception('The given directory does not exist'); 171 | } 172 | 173 | if(!str::startsWith($root, $this->root)) { 174 | throw new Exception('Invalid directory. Must be within the library'); 175 | } 176 | 177 | while($root != $this->root) { 178 | $files = dir::read($root); 179 | if(count($files) === 0) { 180 | dir::remove($root); 181 | } else { 182 | break; 183 | } 184 | $root = dirname($root); 185 | } 186 | 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Library 2 | 3 | *The Library is a file-based, searchable data and file storage solution written in PHP.* 4 | 5 | It's easy to setup and use. It's intended for cheap shared hosting, small virtual servers, your NAS or RaspberryPi and similar thingies where a node.js or Rails based solution is too much or simply not installable. 6 | 7 | It can handle a couple thousand entries without any problems. It's perfect for small personal single-user applications: a self-hosted photo album, a tweet backup, a personal checkins db, an address book, a personal invoice app, a book inventory, a todo app — you name it. 8 | 9 | The Library is also a perfect combination of any kind of text-based data combined with attached files. Each stored item can have any number of additional images, documents, videos, etc. attached. 10 | 11 | The Library has no schema. You can add any number of fields per item and individual items can have different sets of fields. 12 | 13 | *** 14 | 15 | ## Table of contents 16 | 17 | - [Philosophy](#philosophy) 18 | - [Folder structure](#folder-structure) 19 | - [Sqlite index](#sqlite-index) 20 | - [Requirements](#requirements) 21 | - [Installation](#installation) 22 | - [Getting started](#getting-started) 23 | - [Creating items](#creating-items) 24 | - [Item anatomy](#item-anatomy) 25 | - [Item getters](#item-getters) 26 | - [Additional item methods](#additional-item-methods) 27 | - [Updates](#updates) 28 | - [Queries](#queries) 29 | - [Attachments](#attachments) 30 | - [Deleting items](#deleting-items) 31 | - [Roadmap](#roadmap) 32 | - [License](#license) 33 | - [Author](#author) 34 | 35 | *** 36 | 37 | ## Philosophy 38 | 39 | - practical 40 | - simple 41 | - robust 42 | - low fancy-level 43 | - high usefulness-level 44 | - for everyone 45 | 46 | *** 47 | 48 | ## Folder structure 49 | 50 | The Library stores each item in its own folder. The folder structure follows the creation date of the item: 51 | 52 | ``` 53 | - library 54 | -- 2015 55 | --- 06 56 | ---- 10 57 | ----- 32-char-item-id 58 | ------ item.yaml 59 | ------ attachment-1.jpg 60 | ------ attachment-2.pdf 61 | ------ attachment-3.zip 62 | etc. 63 | ``` 64 | 65 | You can easily have multiple libraries per app and user in different folders. 66 | 67 | *** 68 | 69 | ## SQLite Index 70 | 71 | The Library uses a simple SQLite database as a searchable index and builds on the database class of the Kirby toolkit to provide a nice and clean query api. 72 | 73 | The index is stored in the main directory of the library and can be removed at any time. The Library will take care of rebuilding the index once it's gone. The folder structure, attachments and yaml files are always the original data source, which makes this solution very robust. 74 | 75 | *** 76 | 77 | ## Requirements 78 | 79 | - PHP 5.4+ 80 | - sqlite 81 | - mbstring 82 | 83 | *** 84 | 85 | ## Installation 86 | 87 | ``` 88 | git clone https://github.com/bastianallgeier/library.git 89 | ``` 90 | 91 | *** 92 | 93 | ## Getting started 94 | 95 | ```php 96 | require('library/bootstrap.php'); 97 | 98 | $library = new Library(__DIR__ . '/mylibrary'); 99 | ``` 100 | 101 | Make sure the library folder is writable. Otherwise the library will not be able to store any data for you. 102 | 103 | *** 104 | 105 | ## Creating items 106 | 107 | ```php 108 | $item = $library->create('article', array( 109 | 'title' => 'Hello World', 110 | 'text' => 'Lorem ipsum…' 111 | )); 112 | ``` 113 | 114 | *** 115 | 116 | ## Item anatomy 117 | 118 | Each item is stored in its own folder. The path to the folder follows the format YYYY/MM/DD/ID. The creation date is used herefor. Each item has the following default fields: 119 | 120 | ``` 121 | - id (unique 32 char alphanumeric string) 122 | - status (draft|public|private) 123 | - type (can be any alphanumeric string between 2 and 32 characters) 124 | - created (unix timestamp) 125 | - updated (unix timestamp) 126 | ``` 127 | 128 | Additionally you can add any number of fields to an item. 129 | 130 | *** 131 | 132 | ## Item getters 133 | 134 | ```php 135 | // standard getters 136 | $item->id(); 137 | $item->status(); 138 | $item->type(); 139 | $item->created(); 140 | $item->updated(); 141 | 142 | // magic getters for your additional fields 143 | $item->title(); 144 | $item->text(); 145 | // etc. 146 | ``` 147 | 148 | *** 149 | 150 | ## Additional item methods 151 | 152 | ```php 153 | // i.e. 2012/12/12/5gWsOULCwbkInoVQbJ0MxcjybX5hfkv9 154 | $item->path(); 155 | // i.e. 2012 156 | $item->path('year'); 157 | // i.e. 2012/12 158 | $item->path('month'); 159 | // i.e. 2012/12/12 160 | $item->path('day'); 161 | 162 | // i.e. /var/www/library/2012/12/12/5gWsOULCwbkInoVQbJ0MxcjybX5hfkv9 163 | $item->root(); 164 | // i.e. /var/www/library/2012 165 | $item->root('year'); 166 | // i.e. /var/www/library/2012/12 167 | $item->root('month'); 168 | // i.e. /var/www/library/2012/12/12 169 | $item->root('day'); 170 | 171 | // Kirby toolkit folder object 172 | // http://getkirby.com/docs/toolkit/api#folder 173 | $item->folder(); 174 | 175 | // returns an associative array of all item fields 176 | $item->toArray(); 177 | 178 | // checks if the item exists 179 | $item->exists(); 180 | ``` 181 | Check the following examples for more specific stuff. 182 | 183 | *** 184 | 185 | ## Updates 186 | 187 | ```php 188 | $item->update(array( 189 | 'title' => 'New title', 190 | 'text' => 'Lorem ipsum dolor sit amet', 191 | 'date' => time() 192 | )); 193 | ``` 194 | 195 | ### Magic Setters 196 | 197 | ```php 198 | $item->title = 'New title'; 199 | $item->text = 'Lorem ipsum…'; 200 | 201 | $item->store(); 202 | ``` 203 | 204 | ### Set method 205 | 206 | ```php 207 | $item->set('title', 'New title'); 208 | $item->set('text', 'Lorem ipsum…'); 209 | 210 | $item->store(); 211 | ``` 212 | 213 | ### Setting multiple values 214 | 215 | ```php 216 | $item->set(array( 217 | 'title' => 'New title', 218 | 'text' => 'Lorem ipsum dolor sit amet' 219 | )); 220 | 221 | $item->store(); 222 | ``` 223 | 224 | ### Modifying the type 225 | 226 | ```php 227 | $item->type('blogpost'); 228 | ``` 229 | 230 | ### Modifying the creation date 231 | 232 | ```php 233 | $item->created('2012-12-12 22:33'); 234 | ``` 235 | 236 | ### Switching the status 237 | 238 | ```php 239 | $item->status('public'); 240 | ``` 241 | 242 | Available statuses: draft (default), public, private 243 | 244 | *** 245 | 246 | ## Queries 247 | 248 | ### Finding a single item by id 249 | 250 | ```php 251 | $item = $library->find('ekM9AZMIWbkm48hlpRCJO52FVCQSkClL'); 252 | ``` 253 | 254 | ### All items from the library 255 | 256 | ```php 257 | $items = $library->all(); 258 | ``` 259 | 260 | ### Pagination 261 | 262 | ```php 263 | $items = $library->page($page, $limit); 264 | ``` 265 | 266 | ### Counting all items 267 | 268 | ```php 269 | $count = $library->count(); 270 | ``` 271 | 272 | ### Filtering the library by type 273 | 274 | ```php 275 | $items = $library->type('article')->all(); 276 | $items = $library->type('article')->page($page, $limit); 277 | $count = $library->type('article')->count(); 278 | ``` 279 | 280 | ### Filtering the library by status 281 | 282 | ```php 283 | $items = $library->status('draft')->all(); 284 | $items = $library->status('public')->page($page, $limit); 285 | $count = $library->status('private')->count(); 286 | ``` 287 | 288 | ### Filtering the library by year 289 | 290 | ```php 291 | $items = $library->year('2015')->all(); 292 | $items = $library->year('2015')->page($page, $limit); 293 | $count = $library->year('2015')->count(); 294 | ``` 295 | 296 | ### Filtering the library by month 297 | 298 | ```php 299 | $items = $library->month('2015-06')->all(); 300 | $items = $library->month('2015-06')->page($page, $limit); 301 | $count = $library->month('2015-06')->count(); 302 | ``` 303 | 304 | ### Filtering the library by day 305 | 306 | ```php 307 | $items = $library->day('2015-06-10')->all(); 308 | $items = $library->day('2015-06-10')->page($page, $limit); 309 | $count = $library->day('2015-06-10')->count(); 310 | ``` 311 | 312 | ### Searching the library 313 | 314 | ```php 315 | $items = $library->search($query)->all(); 316 | $items = $library->search($query)->page($page, $limit); 317 | $count = $library->search($query)->count(); 318 | ``` 319 | 320 | ### Combining filters 321 | 322 | ```php 323 | $items = $library->year('2015')->type('article')->status('public')->search($query)->all(); 324 | $items = $library->year('2015')->type('article')->status('public')->search($query)->page($page, $limit); 325 | $count = $library->year('2015')->type('article')->status('public')->search($query)->count(); 326 | ``` 327 | 328 | *** 329 | 330 | ## Attachments 331 | 332 | ### Attaching a file via URL 333 | 334 | ```php 335 | $item->attach('http://example.com/image.jpg'); 336 | ``` 337 | 338 | ### Attaching a file from the file system 339 | 340 | ```php 341 | $item->attach('/some/path/image.jpg'); 342 | ``` 343 | 344 | ### Setting a custom filename 345 | 346 | ```php 347 | $item->attach('/some/path/image.jpg', 'myimage.jpg'); 348 | ``` 349 | 350 | ### Fetching attachments for an item 351 | 352 | ```php 353 | // all files 354 | $files = $item->files(); 355 | 356 | // all images 357 | $images = $item->images(); 358 | 359 | // all videos 360 | $videos = $item->videos(); 361 | 362 | // all documents 363 | $documents = $item->documents(); 364 | ``` 365 | 366 | All methods above return a Kirby Toolkit Collection of Media objects: 367 | - 368 | - 369 | 370 | ### Deleting an attachment 371 | 372 | ```php 373 | $item->detach('myimage.jpg); 374 | ``` 375 | 376 | *** 377 | 378 | ## Deleting items 379 | 380 | ### Deleting a single item 381 | 382 | ```php 383 | $item->delete(); 384 | ``` 385 | 386 | ## Deleting multiple items from the library 387 | 388 | ```php 389 | $library->type('article')->delete(); 390 | ``` 391 | 392 | *** 393 | 394 | ## Roadmap 395 | 396 | - more docs 397 | - "between" query method for dates 398 | - maybe geo queries 399 | - Travis setup 400 | - Composer support 401 | 402 | *** 403 | 404 | ## License 405 | 406 | 407 | 408 | *** 409 | 410 | ## Author 411 | 412 | Bastian Allgeier 413 | - 414 | - 415 | - 416 | -------------------------------------------------------------------------------- /library/item.php: -------------------------------------------------------------------------------- 1 | library = $library; 29 | 30 | // set the type 31 | $this->type = $type; 32 | 33 | // add all fields 34 | $this->set($data); 35 | 36 | // auto-create an id for new items 37 | if(!isset($this->data['id'])) { 38 | $this->id = $library->id(); 39 | } 40 | 41 | // auto-set the status to draft if nothing is set 42 | if(!isset($this->data['status'])) { 43 | $this->status = 'draft'; 44 | } 45 | 46 | // auto-set the updated timestamp 47 | if(!isset($this->data['updated'])) { 48 | $this->updated = time(); 49 | } 50 | 51 | // auto-set the created timestamp 52 | if(!isset($this->data['created'])) { 53 | $this->created = time(); 54 | } 55 | 56 | // validate the object 57 | $this->validate(); 58 | 59 | } 60 | 61 | public function validate() { 62 | 63 | // check for a valid library object 64 | if(!is_a($this->library, 'Library')) { 65 | throw new Exception('The library object is invalid'); 66 | } 67 | 68 | // check for all required fields 69 | foreach(static::$required as $field) { 70 | if(empty($this->data[$field])) { 71 | throw new Exception('Missing required field: ' . $field); 72 | } 73 | } 74 | 75 | // id validation 76 | if(!is_string($this->data['id']) or !v::alphanum($this->data['id']) or str::length($this->data['id']) !== 32) { 77 | throw new Exception('Invalid id'); 78 | } 79 | 80 | // type validation 81 | if(!is_string($this->data['type']) or !v::between($this->data['type'], 2, 32)) { 82 | throw new Exception('Invalid type'); 83 | } 84 | 85 | // status validation 86 | if(!in_array($this->data['status'], static::$statuses)) { 87 | throw new Exception('Invalid status: ' . $this->data['status']); 88 | } 89 | 90 | // check for invalid updated timestamp 91 | if(!is_int($this->data['updated']) or !v::between(date('Y', $this->data['updated']), 1980, 2500)) { 92 | throw new Exception('Invalid updated timestamp'); 93 | } 94 | 95 | // check for invalid created timestamp 96 | if(!is_int($this->data['created']) or !v::between(date('Y', $this->data['created']), 1980, 2500) or $this->data['created'] > time()) { 97 | throw new Exception('Invalid created timestamp'); 98 | } 99 | 100 | } 101 | 102 | static public function create(Library $library, $type, $data = array()) { 103 | $item = new static($library, $type, $data); 104 | return $item->store(); 105 | } 106 | 107 | public function __set($key, $value) { 108 | 109 | // convert nulls to empty strings 110 | if(is_null($value)) $value = ''; 111 | 112 | // avoid invalid values 113 | if(!is_scalar($value)) { 114 | throw new Exception('Invalid value type'); 115 | } 116 | 117 | // avoid invalid keys 118 | if(!is_string($key)) { 119 | throw new Exception('Invalid key type'); 120 | } 121 | 122 | // sanitize the key 123 | $key = str_replace('-', '_', str::slug($key)); 124 | 125 | // sanitize the timestamps 126 | if($key == 'updated' or $key == 'created') { 127 | $value = intval($value); 128 | } 129 | 130 | // clean string values 131 | if(is_string($value)) { 132 | $value = trim($value); 133 | } 134 | 135 | // store the last state 136 | if(isset($this->data[$key])) { 137 | $this->old[$key] = $this->$key; 138 | } else { 139 | $this->old[$key] = $value; 140 | } 141 | 142 | // store it 143 | $this->data[$key] = $value; 144 | 145 | } 146 | 147 | public function __get($key) { 148 | $key = str::slug($key); 149 | if(isset($this->data[$key])) { 150 | return $this->data[$key]; 151 | } else { 152 | return null; 153 | } 154 | } 155 | 156 | public function get($key) { 157 | return $this->__get($key); 158 | } 159 | 160 | public function __call($key, $args) { 161 | return $this->__get($key); 162 | } 163 | 164 | public function set($key, $value = null) { 165 | if(is_array($key)) { 166 | foreach($key as $k => $v) { 167 | $this->__set($k, $v); 168 | } 169 | return $this; 170 | } 171 | $this->__set($key, $value); 172 | return $this; 173 | } 174 | 175 | public function status($status = null) { 176 | 177 | if(is_null($status)) { 178 | return $this->status; 179 | } 180 | 181 | $this->status = $status; 182 | $this->store(); 183 | 184 | return $this; 185 | 186 | } 187 | 188 | public function type($type = null) { 189 | 190 | if(is_null($type)) { 191 | return $this->type; 192 | } 193 | 194 | $this->type = $type; 195 | $this->store(); 196 | 197 | return $this; 198 | 199 | } 200 | 201 | public function created($created = null) { 202 | 203 | if(is_null($created)) { 204 | return $this->created; 205 | } 206 | 207 | // handles all parseable strings 208 | if(!is_int($created)) { 209 | $created = strtotime($created); 210 | } 211 | 212 | // store the new creation date and validate it 213 | $this->created = $created; 214 | $this->store(); 215 | 216 | return $this; 217 | 218 | } 219 | 220 | public function update($data = array()) { 221 | 222 | $this->set($data); 223 | $this->set('updated', time()); 224 | $this->store(); 225 | 226 | return $this; 227 | 228 | } 229 | 230 | public function attach($file, $filename = null) { 231 | 232 | // if the item has not been stored yet 233 | // throw an exception 234 | if(!$this->exists()) { 235 | throw new Exception('Unstored item'); 236 | } 237 | 238 | // filename fallback 239 | if(is_null($filename)) { 240 | $filename = basename($file); 241 | } 242 | 243 | // sanitize the filename 244 | $filename = f::safeName($filename); 245 | 246 | // the item.yaml cannot be overwritten 247 | if($filename == 'item.yaml') { 248 | throw new Exception('item.yaml is a reserved filename'); 249 | } 250 | 251 | // files cannot be overwritten 252 | if(file_exists($this->root() . DS . $filename)) { 253 | throw new Exception('The file exists and cannot be overwritten'); 254 | } 255 | 256 | // attach a remote url 257 | if(v::url($file)) { 258 | $response = remote::get($file); 259 | if($response->code() < 400) { 260 | if(!f::write($this->root() . DS . $filename, $response->content())) { 261 | throw new Exception('The file could not be saved'); 262 | } 263 | } else { 264 | throw new Exception('The file could not be fetched'); 265 | } 266 | } else if(file_exists($file)) { 267 | if(!f::copy($file, $this->root() . DS . $filename)) { 268 | throw new Exception('The file could not be copied'); 269 | } 270 | } 271 | 272 | } 273 | 274 | public function detach($filename) { 275 | 276 | $filename = f::safeName($filename); 277 | 278 | if($filename == 'item.yaml') { 279 | throw new Exception('The item.yaml file cannot be removed'); 280 | } 281 | 282 | if(!f::remove($this->root() . DS . $filename)) { 283 | throw new Exception('The file cannot be removed'); 284 | } 285 | 286 | } 287 | 288 | public function delete() { 289 | 290 | // get this before the item is killed 291 | $dayroot = $this->root('day'); 292 | 293 | if(!dir::remove($this->root())) { 294 | throw new Exception('The item directory could not be removed'); 295 | } 296 | 297 | if(!$this->library->database->query('delete from items where id = :id', array('id' => $this->id))) { 298 | throw new Exception('The delete query failed'); 299 | } 300 | 301 | // make sure to clean up the directory attic 302 | $this->library->clean($dayroot); 303 | 304 | } 305 | 306 | public function toArray() { 307 | return $this->data; 308 | } 309 | 310 | public function exists() { 311 | return is_dir($this->root()); 312 | } 313 | 314 | public function store() { 315 | 316 | // make sure the item is valid before storing it 317 | $this->validate(); 318 | 319 | // make sure the directory is at the right position 320 | $this->relocate(); 321 | 322 | $index = $this->library->index(); 323 | $data = $this->toArray(); 324 | $file = $this->root() . DS . 'item.yaml'; 325 | $columns = $this->library->columns()->pluck('name'); 326 | $missing = array_diff(array_keys($data), $columns); 327 | 328 | // add additional columns 329 | foreach($missing as $column) { 330 | $this->library->database->query('alter table items add ' . $column . ' text'); 331 | } 332 | 333 | // store the data in the file 334 | data::write($file, $data); 335 | 336 | // clean the index first 337 | $this->library->database->query('delete from items where id = :id', array('id' => $this->id)); 338 | 339 | // and then re-add 340 | $index->insert($data); 341 | 342 | return $this; 343 | 344 | } 345 | 346 | public function folder() { 347 | return new Folder($this->root()); 348 | } 349 | 350 | public function files() { 351 | return $this->folder()->files()->not('item.yaml'); 352 | } 353 | 354 | public function images() { 355 | return $this->files()->filterBy('type', 'image'); 356 | } 357 | 358 | public function videos() { 359 | return $this->files()->filterBy('type', 'video'); 360 | } 361 | 362 | public function documents() { 363 | return $this->files()->filterBy('type', 'document'); 364 | } 365 | 366 | public function path($type = null) { 367 | 368 | switch(strtolower($type)) { 369 | case 'year': 370 | return date('Y', $this->created); 371 | case 'month': 372 | return date('Y/m', $this->created); 373 | case 'day': 374 | return date('Y/m/d', $this->created); 375 | default: 376 | return date('Y/m/d', $this->created) . '/' . $this->id; 377 | } 378 | 379 | } 380 | 381 | public function root($type = null) { 382 | return $this->library->root . DS . str_replace('/', DS, $this->path($type)); 383 | } 384 | 385 | protected function relocate() { 386 | 387 | if($this->old['created'] == $this->data['created']) return; 388 | 389 | $old = clone $this; 390 | $old->created = $this->old['created']; 391 | 392 | if(!$old->exists()) return; 393 | 394 | if(!dir::make($this->root())) { 395 | throw new Exception('The new directory could not be created'); 396 | } 397 | 398 | if(!dir::move($old->root(), $this->root())) { 399 | throw new Exception('The directory could not be moved'); 400 | } 401 | 402 | $this->library->clean($old->root('day')); 403 | 404 | } 405 | 406 | } -------------------------------------------------------------------------------- /test/ItemTest.php: -------------------------------------------------------------------------------- 1 | library, null); 15 | } 16 | 17 | public function testSuccessfulItemCreation() { 18 | $item = new Item($this->library, 'todo'); 19 | $this->assertInstanceOf('Library\\Item', $item); 20 | } 21 | 22 | /** 23 | * @expectedException Exception 24 | * @expectedExceptionMessage Invalid id 25 | */ 26 | public function testCreateItemWithInvalidShortId() { 27 | $item = new Item($this->library, 'todo', array( 28 | 'id' => 'abc', 29 | )); 30 | } 31 | 32 | /** 33 | * @expectedException Exception 34 | * @expectedExceptionMessage Invalid id 35 | */ 36 | public function testCreateItemWithInvalidCharInId() { 37 | $item = new Item($this->library, 'todo', array( 38 | 'id' => '#abGuMgoNV5HfbRZbJFCIbDHMXmATJ8d' 39 | )); 40 | } 41 | 42 | 43 | /** 44 | * @expectedException Exception 45 | * @expectedExceptionMessage Invalid type 46 | */ 47 | public function testCreateItemWithInvalidCharsInType() { 48 | 49 | // not a string 50 | $item = new Item($this->library, 1); 51 | 52 | } 53 | 54 | /** 55 | * @expectedException Exception 56 | * @expectedExceptionMessage Invalid type 57 | */ 58 | public function testCreateItemWithInvalidShortType() { 59 | $item = new Item($this->library, 't'); 60 | } 61 | 62 | /** 63 | * @expectedException Exception 64 | * @expectedExceptionMessage Invalid type 65 | */ 66 | public function testCreateItemWithInvalidLongType() { 67 | $item = new Item($this->library, 'taskdasdhkajhsdkjahskjdhakjsdhkjahsdkjahsdk'); 68 | } 69 | 70 | /** 71 | * @expectedException Exception 72 | * @expectedExceptionMessage Invalid status 73 | */ 74 | public function testCreateItemWithInvalidStatus() { 75 | $item = new Item($this->library, 'todo', array( 76 | 'status' => 'abc' 77 | )); 78 | } 79 | 80 | /** 81 | * @expectedException Exception 82 | * @expectedExceptionMessage Invalid updated timestamp 83 | */ 84 | public function testCreateItemWithInvalidUpdatedTimestamp() { 85 | 86 | $item = new Item($this->library, 'todo', array( 87 | 'updated' => strtotime('1900-01-01') 88 | )); 89 | 90 | $item = new Item($this->library, 'todo', array( 91 | 'updated' => strtotime('2900-01-01') 92 | )); 93 | 94 | } 95 | 96 | /** 97 | * @expectedException Exception 98 | * @expectedExceptionMessage Invalid created timestamp 99 | */ 100 | public function testCreateItemWithInvalidCreatedTimestamp() { 101 | 102 | $item = new Item($this->library, 'todo', array( 103 | 'created' => strtotime('1900-01-01') 104 | )); 105 | 106 | $item = new Item($this->library, 'todo', array( 107 | 'created' => strtotime('2900-01-01') 108 | )); 109 | 110 | $item = new Item($this->library, 'todo', array( 111 | 'created' => time() + 10 112 | )); 113 | 114 | } 115 | 116 | public function testCreate() { 117 | 118 | $item = Item::create($this->library, 'todo'); 119 | 120 | $this->assertEquals('todo', $item->type()); 121 | $this->assertEquals('draft', $item->status()); 122 | 123 | // search for the item in the library 124 | $this->assertEquals($item, $this->library->find($item->id)); 125 | 126 | } 127 | 128 | /** 129 | * @expectedException Exception 130 | * @expectedExceptionMessage Invalid value type 131 | */ 132 | public function testSetWithInvalidArrayValue() { 133 | 134 | $item = $this->item(); 135 | $item->tags = array('a','b','c'); 136 | 137 | } 138 | 139 | /** 140 | * @expectedException Exception 141 | * @expectedExceptionMessage Invalid value type 142 | */ 143 | public function testSetWithInvalidObjectValue() { 144 | 145 | $item = $this->item(); 146 | $item->tags = new stdClass; 147 | 148 | } 149 | 150 | /** 151 | * @expectedException Exception 152 | * @expectedExceptionMessage Invalid value type 153 | */ 154 | public function testSetWithInvalidResourceValue() { 155 | 156 | $item = $this->item(); 157 | $item->tags = tmpfile(); 158 | 159 | } 160 | 161 | /** 162 | * @expectedException Exception 163 | * @expectedExceptionMessage Invalid key type 164 | */ 165 | public function testSetWithInvalidKey() { 166 | 167 | $item = $this->item(); 168 | $item->set(1, 'test'); 169 | 170 | } 171 | 172 | public function testMultipleSet() { 173 | 174 | $item = $this->item(); 175 | $item->set(array( 176 | 'keyA' => 'valueA', 177 | 'keyB' => 'valueB' 178 | )); 179 | 180 | $this->assertEquals('valueA', $item->keyA); 181 | $this->assertEquals('valueB', $item->keyB); 182 | 183 | } 184 | 185 | public function testSet() { 186 | 187 | // Version A 188 | $item = $this->item(); 189 | $item->title = 'test'; 190 | 191 | $this->assertEquals('test', $item->title); 192 | 193 | // Version B 194 | $item = $this->item(); 195 | $item->set('title', 'test'); 196 | 197 | $this->assertEquals('test', $item->title); 198 | 199 | // Version C 200 | $item = $this->item(); 201 | $item->__set('title', 'test'); 202 | 203 | $this->assertEquals('test', $item->title); 204 | 205 | } 206 | 207 | public function testGet() { 208 | 209 | $item = $this->item(); 210 | $item->title = 'test'; 211 | 212 | $this->assertEquals('test', $item->title); 213 | $this->assertEquals('test', $item->title()); 214 | $this->assertEquals('test', $item->get('title')); 215 | $this->assertEquals('test', $item->__get('title')); 216 | 217 | // different key versions 218 | $this->assertEquals('test', $item->Title); 219 | $this->assertEquals('test', $item->TITLE); 220 | $this->assertEquals('test', $item->tItLe); 221 | 222 | } 223 | 224 | public function testPath() { 225 | 226 | $item = $this->item(); 227 | 228 | $this->assertEquals(date('Y'), $item->path('year')); 229 | $this->assertEquals(date('Y/m'), $item->path('month')); 230 | $this->assertEquals(date('Y/m/d'), $item->path('day')); 231 | $this->assertEquals(date('Y/m/d') . DS . $item->id, $item->path()); 232 | 233 | } 234 | 235 | public function testRoot() { 236 | 237 | $item = $this->item(); 238 | 239 | $this->assertEquals($this->root . DS . date('Y'), $item->root('year')); 240 | $this->assertEquals($this->root . DS . date('Y/m'), $item->root('month')); 241 | $this->assertEquals($this->root . DS . date('Y/m/d'), $item->root('day')); 242 | $this->assertEquals($this->root . DS . date('Y/m/d') . DS . $item->id, $item->root()); 243 | 244 | } 245 | 246 | public function testStatus() { 247 | 248 | $item = $this->item(); 249 | 250 | $this->assertEquals('draft', $item->status); 251 | 252 | $item->status('public'); 253 | 254 | $this->assertEquals('public', $item->status); 255 | 256 | $item->status('private'); 257 | 258 | $this->assertEquals('private', $item->status); 259 | 260 | $item->status('draft'); 261 | 262 | $this->assertEquals('draft', $item->status); 263 | 264 | } 265 | 266 | public function testType() { 267 | 268 | $item = $this->item(); 269 | 270 | $this->assertEquals('test', $item->type); 271 | 272 | $item->type('sometype'); 273 | 274 | $this->assertEquals('sometype', $item->type); 275 | 276 | } 277 | 278 | public function testCreated() { 279 | 280 | $item = $this->item(); 281 | 282 | $this->assertEquals(time(), $item->created); 283 | 284 | $item->created('2012-12-12 22:33'); 285 | 286 | $this->assertEquals(strtotime('2012-12-12 22:33'), $item->created); 287 | 288 | } 289 | 290 | public function testUpdate() { 291 | 292 | $item = $this->item(); 293 | $item->title = 'A'; 294 | $item->updated = time() - 10; 295 | $item->store(); 296 | 297 | $updated = $item->updated(); 298 | 299 | $this->assertEquals('A', $item->title); 300 | $this->assertEquals(1, $this->library->index->where('title', '=', 'A')->count()); 301 | $this->assertEquals(0, $this->library->index->where('title', '=', 'B')->count()); 302 | 303 | $item->update(array( 304 | 'title' => 'B' 305 | )); 306 | 307 | $this->assertEquals('B', $item->title); 308 | $this->assertEquals(0, $this->library->index->where('title', '=', 'A')->count()); 309 | $this->assertEquals(1, $this->library->index->where('title', '=', 'B')->count()); 310 | 311 | $this->assertTrue($updated < $item->updated()); 312 | 313 | } 314 | 315 | public function testDelete() { 316 | 317 | $item = $this->item(); 318 | $root = $item->root(); 319 | $id = $item->id(); 320 | 321 | $item->store(); 322 | 323 | $this->assertTrue(is_dir($root)); 324 | $this->assertEquals(1, $this->library->index->where('id', '=', $id)->count()); 325 | 326 | $item->delete(); 327 | 328 | // check for proper cleaning of directories 329 | $this->assertFalse(is_dir($root)); 330 | $this->assertFalse(is_dir(dirname($root))); 331 | $this->assertFalse(is_dir(dirname(dirname($root)))); 332 | $this->assertFalse(is_dir(dirname(dirname(dirname($root))))); 333 | 334 | // check for a clean index table 335 | $this->assertEquals(0, $this->library->index->where('id', '=', $id)->count()); 336 | 337 | } 338 | 339 | public function testToArray() { 340 | 341 | $item = $this->item(); 342 | 343 | $item->type = 'test'; 344 | $item->status = 'public'; 345 | $item->title = 'test'; 346 | 347 | $item->store(); 348 | 349 | $result = array( 350 | 'type' => 'test', 351 | 'id' => $item->id, 352 | 'status' => 'public', 353 | 'updated' => time(), 354 | 'created' => time(), 355 | 'title' => 'test' 356 | ); 357 | 358 | $this->assertEquals($result, $item->toArray()); 359 | 360 | } 361 | 362 | /** 363 | * @expectedException Exception 364 | * @expectedExceptionMessage Unstored item 365 | */ 366 | public function testAttachWithUnstoredItem() { 367 | 368 | $item = $this->item(); 369 | $item->attach('http://domain.com/image.jpg'); 370 | 371 | } 372 | 373 | /** 374 | * @expectedException Exception 375 | * @expectedExceptionMessage The file could not be fetched 376 | */ 377 | public function testAttachWithBrokenLink() { 378 | 379 | $item = $this->item(); 380 | $item->store(); 381 | $item->attach('http://getkirby.com/404.jpg'); 382 | 383 | } 384 | 385 | /** 386 | * @expectedException Exception 387 | * @expectedExceptionMessage item.yaml is a reserved filename 388 | */ 389 | public function testAttachItemYaml() { 390 | 391 | $item = $this->item(); 392 | $item->store(); 393 | $item->attach(__FILE__, 'item.yaml'); 394 | 395 | } 396 | 397 | /** 398 | * @expectedException Exception 399 | * @expectedExceptionMessage The file exists and cannot be overwritten 400 | */ 401 | public function testAttachOverwritingFiles() { 402 | 403 | $item = $this->item(); 404 | $item->store(); 405 | $item->attach(__FILE__, 'test.php'); 406 | $item->attach(__FILE__, 'test.php'); 407 | 408 | } 409 | 410 | public function testAttachFromUrl() { 411 | 412 | $item = $this->item(); 413 | $item->store(); 414 | $item->attach('http://getkirby.com/assets/images/logo.png'); 415 | 416 | $this->assertTrue(file_exists($item->root() . DS . 'logo.png')); 417 | $this->assertEquals('logo.png', $item->images()->first()->filename()); 418 | $this->assertEquals('image/png', $item->images()->first()->mime()); 419 | 420 | } 421 | 422 | public function testAttachFromUrlWithCustomFilename() { 423 | 424 | $item = $this->item(); 425 | $item->store(); 426 | $item->attach('http://getkirby.com/assets/images/logo.png', 'myfile.png'); 427 | 428 | $this->assertTrue(file_exists($item->root() . DS . 'myfile.png')); 429 | $this->assertEquals('myfile.png', $item->images()->first()->filename()); 430 | $this->assertEquals('image/png', $item->images()->first()->mime()); 431 | 432 | } 433 | 434 | public function testAttachFromFile() { 435 | 436 | $item = $this->item(); 437 | $item->store(); 438 | $item->attach(__FILE__); 439 | 440 | $filename = f::safeName(basename(__FILE__)); 441 | 442 | $this->assertTrue(file_exists($item->root() . DS . $filename)); 443 | $this->assertEquals($filename, $item->files()->first()->filename()); 444 | 445 | } 446 | 447 | public function testAttachFromFileWithCustomFilename() { 448 | 449 | $item = $this->item(); 450 | $item->store(); 451 | $item->attach(__FILE__, 'myfile.php'); 452 | 453 | $this->assertTrue(file_exists($item->root() . DS . 'myfile.php')); 454 | $this->assertEquals('myfile.php', $item->files()->first()->filename()); 455 | 456 | } 457 | 458 | public function testDetach() { 459 | 460 | $item = $this->item(); 461 | $item->store(); 462 | 463 | $item->attach(__FILE__, 'myfile.php'); 464 | 465 | $this->assertTrue(file_exists($item->root() . DS . 'myfile.php')); 466 | 467 | $item->detach('myfile.php'); 468 | 469 | $this->assertFalse(file_exists($item->root() . DS . 'myfile.php')); 470 | 471 | } 472 | 473 | /** 474 | * @expectedException Exception 475 | * @expectedExceptionMessage The file cannot be removed 476 | */ 477 | public function testDetachInvalidFile() { 478 | 479 | $item = $this->item(); 480 | 481 | $item->store(); 482 | 483 | $item->detach('myfile.php'); 484 | 485 | } 486 | 487 | /** 488 | * @expectedException Exception 489 | * @expectedExceptionMessage The item.yaml file cannot be removed 490 | */ 491 | public function testDetachItemYaml() { 492 | 493 | $item = $this->item(); 494 | 495 | $item->store(); 496 | 497 | $item->detach('item.yaml'); 498 | 499 | } 500 | 501 | 502 | public function testExists() { 503 | 504 | $item = $this->item(); 505 | 506 | $this->assertFalse($item->exists()); 507 | 508 | $item->store(); 509 | 510 | $this->assertTrue($item->exists()); 511 | 512 | } 513 | 514 | public function testRelocate() { 515 | 516 | $item = $this->item(); 517 | $root = $item->root(); 518 | 519 | $item->store(); 520 | 521 | $this->assertTrue(is_dir($root)); 522 | 523 | $item->created('2012-12-12 22:33'); 524 | 525 | // check for proper cleaning of directories 526 | $this->assertFalse(is_dir($root)); 527 | $this->assertFalse(is_dir(dirname($root))); 528 | $this->assertFalse(is_dir(dirname(dirname($root)))); 529 | $this->assertFalse(is_dir(dirname(dirname(dirname($root))))); 530 | 531 | // check for a new root 532 | $this->assertTrue(is_dir($item->root())); 533 | 534 | } 535 | 536 | public function testColumnAddition() { 537 | 538 | $item = $this->item(); 539 | $columns = $this->library->columns()->pluck('name'); 540 | $expected = array( 541 | 'id', 542 | 'type', 543 | 'status', 544 | 'created', 545 | 'updated' 546 | ); 547 | 548 | $this->assertEquals($expected, $columns); 549 | 550 | $item->update(array( 551 | 'title' => 'test' 552 | )); 553 | 554 | // check the list of columns again 555 | $columns = $this->library->columns()->pluck('name'); 556 | 557 | // the list of columns should now also have the title 558 | $expected[] = 'title'; 559 | 560 | $this->assertEquals($expected, $columns); 561 | 562 | } 563 | 564 | } --------------------------------------------------------------------------------