├── .coveralls.yml ├── .editorconfig ├── .gitignore ├── .nitpick.json ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── build ├── bin │ └── build-docs.sh └── sami-config.php ├── composer.json ├── phpunit.xml.dist ├── src ├── Contracts │ └── Type │ │ └── Registerable.php ├── Event │ └── Hook.php ├── Exception │ └── WP_ErrorException.php ├── Meta │ ├── Meta.php │ └── ObjectMeta.php ├── Post │ ├── ClassNameAsPostType.php │ ├── Exception │ │ ├── ModelPostTypeMismatchException.php │ │ └── PostNotFoundException.php │ ├── Model.php │ └── QueryBuilder.php ├── PostType │ ├── Builder.php │ ├── Exception │ │ ├── InvalidPostTypeNameException.php │ │ └── NonExistentPostTypeException.php │ └── PostType.php ├── Query │ └── Builder.php ├── Silk.php ├── Support │ ├── Callback.php │ ├── Collection.php │ └── Shortcode.php ├── Taxonomy │ ├── Builder.php │ ├── Exception │ │ ├── InvalidTaxonomyNameException.php │ │ └── NonExistentTaxonomyException.php │ └── Taxonomy.php ├── Term │ ├── Exception │ │ ├── TaxonomyMismatchException.php │ │ └── TermNotFoundException.php │ ├── Model.php │ └── QueryBuilder.php ├── Type │ ├── Builder.php │ ├── Labels.php │ ├── Model.php │ ├── ObjectAliases.php │ ├── ShorthandProperties.php │ └── Type.php ├── User │ ├── Exception │ │ └── UserNotFoundException.php │ ├── Model.php │ └── QueryBuilder.php ├── WordPress │ ├── Post │ │ ├── Attachment.php │ │ ├── NavMenuItem.php │ │ ├── Page.php │ │ ├── Post.php │ │ └── Revision.php │ ├── Term │ │ ├── Category.php │ │ ├── LinkCategory.php │ │ ├── NavMenu.php │ │ ├── PostFormat.php │ │ └── Tag.php │ └── User │ │ └── User.php └── functions.php └── tests ├── bootstrap.php ├── src └── TermFactoryHelpers.php ├── unit ├── Event │ └── HookTest.php ├── Meta │ ├── MetaTest.php │ └── ObjectMetaTest.php ├── Post │ ├── PageTest.php │ ├── PostModelTest.php │ ├── PostQueryBuilderTest.php │ └── PostTest.php ├── PostType │ ├── PostTypeAssertions.php │ ├── PostTypeBuilderTest.php │ └── PostTypeTest.php ├── Shortcode │ └── ShortcodeTest.php ├── Support │ └── CallbackTest.php ├── Taxonomy │ ├── TaxonomyBuilderTest.php │ └── TaxonomyTest.php ├── Term │ ├── TermQueryBuilderTest.php │ └── TermTest.php └── User │ ├── UserModelTest.php │ └── UserQueryBuilderTest.php └── wp-config.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | coverage_clover: build/logs/clover.xml 2 | service_name: travis-ci 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /wordpress/ 3 | /build/logs/ 4 | composer.lock 5 | .idea 6 | *.phar 7 | -------------------------------------------------------------------------------- /.nitpick.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "tests/*" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | env: 4 | global: 5 | - WP_DB_USER=wp 6 | - WP_DB_PASS=password 7 | - WP_DB_NAME=wp_tests 8 | 9 | matrix: 10 | include: 11 | - php: 7.2 12 | env: WP_VERSION=* BUILD_DOCS=1 13 | - php: 7.1 14 | env: WP_VERSION=* 15 | - php: 7.0 16 | env: WP_VERSION=* 17 | - php: 5.6 18 | env: WP_VERSION=* 19 | - php: 5.6 20 | env: WP_VERSION=4.5.3 21 | 22 | before_install: 23 | - composer validate --strict 24 | 25 | install: 26 | - composer install 27 | - composer require --dev --update-with-dependencies johnpbloch/wordpress:$WP_VERSION wp-phpunit/wp-phpunit:$WP_VERSION 28 | - composer show 29 | 30 | before_script: 31 | - mysql -u root -e "GRANT ALL PRIVILEGES ON ${WP_DB_NAME}.* TO ${WP_DB_USER} IDENTIFIED BY '${WP_DB_PASS}';" 32 | - mysql -u root -e "CREATE DATABASE ${WP_DB_NAME};" 33 | - mkdir -p build/logs 34 | 35 | script: composer test 36 | 37 | after_success: travis_retry composer coverage -- -v 38 | 39 | before_deploy: bash build/bin/build-docs.sh 40 | 41 | deploy: 42 | provider: s3 43 | access_key_id: AKIAIUOEMT3YXWAPTEJA 44 | secret_access_key: 45 | secure: Wqmlzs52YkR3mzmW4tvFSFincFyiJnFJvtpxsiP7wdbOqefjYiZlF5jCn6rpXvwfm3OUSFzplSDzeWUKbHjPPDkIP82Zk6jbdFPHqDurkoBdfdkIkD/X/yfQsMTDVfchRZbWaNj/+IiykuHBDkljvhbaZtepODJMIkBjkZom4Yb3pjOUARgBGy2gtI4WhaVyg4oZ1Xm0o4S6i5AUGxIZjOBfCwePhsioEZFDsaV8QVWfa7K3ttlJtpGFDLhFzB29mMVXLWbTc8I7DoEu8ghJ0bq2ilO/dRxqy3IZN7ZkLockKbg+UM4xyp/b+yQtIvmN5DMM2m3aswyz8snLqrhtKRE1QjH4/UJUunLkvn79UotXm6Vl9HTnDYG5DimsMOGxVGEuWUsY/pN0tRqPA9HGjSqOeEzgj+OjOIlUssv/E1+5+OYtpBGOnvl6tzyAffQdhdhJ+wZgOsLs9U5zRnp+t3+TAvfN6+SPmG/SQX0T7mRIZNVhICH9p/gpqjtljBJy724Xx/ZquW/tkFLB6X/M97p4OBEtX+MvwCE0L/lV2/uT2fZV3BqdYYX7fCakpWVwEA7c2pxW8VyjZf97iindxh9HQrGAu7QT3lOGyIRYsCfWgfC4yPJcMWjJZj6d5uQvVrJtMOQzVo4y9HQ9b0iXe9LYQrjdLa4xBDgLatZw2W8= 46 | bucket: api.silk.aaemnnost.tv 47 | acl: public_read 48 | skip_cleanup: true 49 | local_dir: ../silk-api-docs 50 | on: 51 | branch: master 52 | php: 7.2 53 | condition: $BUILD_DOCS 54 | 55 | cache: 56 | directories: 57 | - vendor 58 | 59 | notifications: 60 | email: 61 | on_success: never 62 | on_failure: change 63 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## v0.12.1 (2018-08-06) 4 | 5 | - Fix location of `UserNotFoundException` 6 | - Formatting tweaks 7 | 8 | ## v0.12.0 (2018-03-02) 9 | 10 | ### Added 11 | - [Query scopes](https://github.com/aaemnnosttv/silk/issues/24) for model queries 12 | - [Soft `Model::find(id)` retrieval method](https://github.com/aaemnnosttv/silk/issues/25) 13 | - `\Silk\Silk::VERSION` Version constant 14 | 15 | ## Changed 16 | - Raised minimum PHP version to 5.6 17 | - [Updated `tightenco/collect` Collection library](https://github.com/aaemnnosttv/silk/issues/27) 18 | **_Breaking change from previous version with some methods_** (see issue) 19 | - [Removed deprecated methods](https://github.com/aaemnnosttv/silk/issues/28) **Breaking Change** 20 | - `Silk\Post\Model::fromWpPost` 21 | - `Silk\Term\Model::fromWpTerm` 22 | - Removed `Silk\Contracts\Query\BuildsQueries` interface 23 | 24 | ## v0.11.1 (2016-30-08) 25 | 26 | ### Changed 27 | - Restrict `tightenco/collect` to 5.2 28 | 29 | ## v0.11.0 (2016-18-08) 30 | 31 | ### Added 32 | - `User\Model` 33 | - `User\QueryBuilder` 34 | - `url` methods on `Post\Model` & `Term\Model` 35 | - `Term\Model->children()` 36 | - `Model::make()` named constructor 37 | - `objectAliases` property, allowing for `$model->aliasName == $model->object->targetProperty` 38 | - Shorthand property aliases, Eg: `$postModel->{name} == $postModel->object->post_{name}` (opt-in via trait) 39 | 40 | ### Changed 41 | - `Hook` callbacks now automatically return the first argument passed if nothing is returned. 42 | - `ObjectMeta` now has a fluent `set(key, value)` method. 43 | - Deprecated `Post\Model::fromWpPost()` and `Term\Model::fromWpTerm()` (use `::make()` instead) 44 | - Simplified internals of `Model->save()`, `->delete()` and `->refresh()` 45 | 46 | ## v0.10.1 (2016-07-22) 47 | 48 | ### Fixed 49 | - Strict notice on PHP 5 for abstract static method 50 | 51 | ## v0.10.0 (2016-07-16) 52 | 53 | ### Added 54 | - `Term\Model` 55 | - `Taxonomy\Taxonomy` 56 | - `Term\QueryBuilder` 57 | - Conditional Hooks with `onlyIf(callback)` method 58 | - `Meta->replace(old, new)` method 59 | - Models for all WordPress Post Types and Taxonomies 60 | 61 | ### Changed 62 | - `ObjectMeta->collect()` now returns a Collection of Meta objects 63 | - `ObjectMeta->all()` now returns an array 64 | - Meta add, set, delete are now fluent methods 65 | - `Post\PostType` is now `PostType\PostType` 66 | - `Post\PostTypeBuilder` is now `PostType\Builder` 67 | 68 | ## v0.9.0 (2016-06-24) 69 | 70 | **Initial release! 🎉** 71 | 72 | ### Added 73 | - Callback 74 | - Hook + helper functions 75 | - Meta 76 | - ObjectMeta 77 | - Post\Model 78 | - PostType 79 | - PostTypeBuilder 80 | - Query\Builder 81 | - Shortcode 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Silk Contribution Guide 2 | 3 | Hey there! Thanks for taking the time to read this. I really appreciate your interest to contribute to Silk! 4 | Before submitting your contribution, please make sure to take a moment and read through the following guidelines. 5 | 6 | ## Issue Reporting Guidelines 7 | 8 | - The issue list of this repo is **exclusively** for bug reports and feature requests. For simple questions, please use [Gitter](https://gitter.im/aaemnnosttv/silk). 9 | 10 | - Try to search for your issue, it may have already been answered. 11 | 12 | - Check if the issue is reproducible with the latest stable version. If you are using a pre-release, please indicate the specific version you are using. 13 | 14 | - If your issue is resolved but still open, don’t hesitate to close it. In case you found a solution by yourself, it could be helpful to explain how you fixed it. 15 | 16 | ## Pull Request Guidelines 17 | 18 | - Checkout a topic branch from `master` and merge back against `master`. 19 | 20 | - Follow the [code style](#code-style). 21 | 22 | - Make sure `phpunit` passes. (see [development setup](#development-setup)) 23 | 24 | - If adding new feature: 25 | - Add accompanying test case. 26 | - Provide convincing reason to add this feature. Ideally you should open a suggestion issue first and have it greenlighted before working on it. 27 | 28 | - If fixing a bug: 29 | - Provide detailed description of the bug in the PR. 30 | - Add appropriate test coverage if applicable. 31 | 32 | ## Code Style 33 | 34 | - Use [PSR-2](http://www.php-fig.org/psr/psr-2/). Just about every text editor has an extension for formatting your code this way. 35 | _Note: This is NOT the same as the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/)._ 36 | 37 | - Code style violations are checked with [Nitpick CI](https://nitpick-ci.com/). 38 | 39 | - Use a text editor that supports [EditorConfig](http://editorconfig.org/) (recommended). 40 | 41 | - When in doubt, read the source code. 42 | 43 | ## Development Setup 44 | 45 | You will need [Composer](https://getcomposer.org/download/). 46 | 47 | - Fork the repository on GitHub if preparing to submit a Pull Request. 48 | 49 | - Clone the repository. 50 | 51 | - Run `composer install` within the project root. 52 | 53 | - Install WordPress test suite 54 | ``` 55 | ./tests/bin/install-wp-tests.sh {db-name} {db-user} {db-pass} {db-host} 56 | ``` 57 | 58 | ## Tests 59 | 60 | Contributions are expected to have test coverage where applicable. Tests are written using PHPUnit. 61 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Evan Mattson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Silk 2 | 3 | [![Travis Build](https://img.shields.io/travis/aaemnnosttv/silk/master.svg)](https://travis-ci.org/aaemnnosttv/silk) 4 | [![Coveralls](https://img.shields.io/coveralls/aaemnnosttv/silk/master.svg)](https://coveralls.io/github/aaemnnosttv/silk?branch=master) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/aaemnnosttv/silk/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/aaemnnosttv/silk/?branch=master) 6 | [![Gitter](https://img.shields.io/gitter/room/aaemnnosttv/silk.svg)](https://gitter.im/aaemnnosttv/silk) 7 | [![Packagist](https://img.shields.io/packagist/v/silk/silk.svg)](https://packagist.org/packages/silk/silk) 8 | [![Packagist](https://img.shields.io/packagist/l/silk/silk.svg)](https://packagist.org/packages/silk/silk) 9 | 10 | 11 | A modern API for WordPress. 12 | 13 | **The current API is still evolving as the project grows closer to v1.0 and may introduce breaking changes.** 14 | 15 | Use with confidence, upgrade with care. 16 | 17 | ## What is Silk? 18 | 19 | Silk is a library designed as a thin layer on top of WordPress, abstracting away the mess of functions into a clean and expressive object-oriented API. Use as much or little as you want, while maintaining compatibility with everything else that's meant to work with WordPress. 20 | 21 | [Read the documentation](https://github.com/aaemnnosttv/silk-docs) for examples and more. 22 | 23 | Or check out the [API documentation](https://api.silk.aaemnnost.tv/master/) if you're into that kind of thing. 24 | 25 | ## Installation 26 | 27 | ```bash 28 | composer require silk/silk 29 | ``` 30 | 31 | ## Contributing 32 | 33 | Contributions are welcome! If you're interested in contributing to the project, please open an issue first. I would hate to decline a Pull Request forged by hours of effort for any reason, so please read the contribution guidelines first. 34 | -------------------------------------------------------------------------------- /build/bin/build-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Use the Phar rather than require it because we don't deploy the docs on every build. 4 | if [ ! -f sami.phar ]; then 5 | curl -O http://get.sensiolabs.org/sami.phar 6 | fi 7 | 8 | # Ensure the working directory is clean 9 | git reset --hard 10 | 11 | # Because Travis only clones a single branch, we only want to build everything on master. 12 | if [ "$TRAVIS_BRANCH" == "master" ]; then 13 | php sami.phar update build/sami-config.php -v --force 14 | else 15 | php sami.phar update build/sami-config.php -vvv --only-version="$TRAVIS_BRANCH" 16 | fi 17 | -------------------------------------------------------------------------------- /build/sami-config.php: -------------------------------------------------------------------------------- 1 | files() 12 | ->name('*.php') 13 | ->in("$repository_root/src") 14 | ; 15 | 16 | // generate documentation for all tags, and the master branch 17 | $versions = GitVersionCollection::create($repository_root) 18 | ->addFromTags('*') 19 | ->add('master', 'master branch') 20 | ; 21 | 22 | return new Sami($iterator, array( 23 | 'versions' => $versions, 24 | 'title' => 'Silk API', 25 | 'build_dir' => __DIR__ . '/../../silk-api-docs/%version%', 26 | 'cache_dir' => __DIR__ . '/../../silk-api-docs-cache/%version%', 27 | 'remote_repository' => new GitHubRemoteRepository('aaemnnosttv/silk', $repository_root), 28 | 'default_opened_level' => 2, 29 | )); 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "silk/silk", 3 | "description": "A modern API for WordPress", 4 | "type": "library", 5 | "authors": [ 6 | { 7 | "name": "Evan Mattson", 8 | "email": "me@aaemnnost.tv", 9 | "homepage": "https://aaemnnost.tv" 10 | } 11 | ], 12 | "support": { 13 | "docs": "https://github.com/aaemnnosttv/silk-docs", 14 | "issues": "https://github.com/aaemnnosttv/silk/issues", 15 | "source": "https://github.com/aaemnnosttv/silk" 16 | }, 17 | "license": "MIT", 18 | "require": { 19 | "php": ">=5.6", 20 | "tightenco/collect": "^5.4" 21 | }, 22 | "require-dev": { 23 | "johnpbloch/wordpress": "^4.9", 24 | "mockery/mockery": "^0.9.5", 25 | "phpunit/phpunit": "^5.0", 26 | "satooshi/php-coveralls": "^1.0", 27 | "wp-phpunit/wp-phpunit": "^4.9" 28 | }, 29 | "autoload": { 30 | "psr-4": {"Silk\\":"src"}, 31 | "files": ["src/functions.php"] 32 | }, 33 | "autoload-dev": { 34 | "classmap": [ 35 | "tests/src" 36 | ] 37 | }, 38 | "scripts": { 39 | "test": "phpunit", 40 | "coverage": "coveralls" 41 | }, 42 | "config": { 43 | "sort-packages": true, 44 | "platform": { 45 | "php": "5.6.36" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/unit/ 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | src 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Contracts/Type/Registerable.php: -------------------------------------------------------------------------------- 1 | handle = $handle; 75 | $this->priority = $priority; 76 | } 77 | 78 | /** 79 | * Set the callback to be invoked by the action or filter. 80 | * 81 | * @param callable $callback The callback to be invoked 82 | * 83 | * @return $this 84 | */ 85 | public function setCallback(callable $callback) 86 | { 87 | $this->callback = new Callback($callback); 88 | $this->callbackParamCount = $this->callback->parameterCount(); 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * Set the hook in WordPress. 95 | * 96 | * Both actions and filters are registered as filters. 97 | * 98 | * @return $this 99 | */ 100 | public function listen() 101 | { 102 | add_filter($this->handle, [$this, 'mediateCallback'], $this->priority, 100); 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Unset the hook in WordPress. 109 | * 110 | * @return $this 111 | */ 112 | public function remove() 113 | { 114 | remove_filter($this->handle, [$this, 'mediateCallback'], $this->priority); 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * Control invocation of the callback. 121 | * 122 | * @param mixed $given The first argument passed to the callback. 123 | * Needed to return for filters. 124 | * 125 | * @return mixed Returned value from Callback 126 | */ 127 | public function mediateCallback($given = null) 128 | { 129 | $arguments = func_get_args(); 130 | 131 | if (! $this->shouldInvoke($arguments)) { 132 | return $given; 133 | } 134 | 135 | if (is_null($returned = $this->invokeCallback($arguments))) { 136 | return $given; 137 | } 138 | 139 | return $returned; 140 | } 141 | 142 | /** 143 | * Whether or not the callback should be invoked. 144 | * 145 | * @param array $arguments All arguments passed to the callback 146 | * 147 | * @return bool 148 | */ 149 | public function shouldInvoke(array $arguments) 150 | { 151 | if ($this->hasExceededIterations()) { 152 | return false; 153 | } 154 | 155 | /** 156 | * Check if any of the conditions returns false, 157 | * if so, do not invoke. 158 | */ 159 | return ! $this->conditions()->contains(function ($callback) use ($arguments) { 160 | return false === $callback->callArray($arguments); 161 | }); 162 | } 163 | 164 | /** 165 | * Call the callback. 166 | * 167 | * @param array $arguments All arguments passed to the callback 168 | * 169 | * @return mixed The value returned from the callback 170 | */ 171 | protected function invokeCallback($arguments) 172 | { 173 | $returned = $this->callback->callArray( 174 | array_slice($arguments, 0, $this->callbackParamCount ?: null) 175 | ); 176 | 177 | $this->iterations++; 178 | 179 | return $returned; 180 | } 181 | 182 | /** 183 | * Set the callback to only be invokable one time. 184 | * 185 | * @return $this 186 | */ 187 | public function once() 188 | { 189 | $this->onlyXtimes(1); 190 | 191 | return $this; 192 | } 193 | 194 | /** 195 | * Set the maximum number of callback invocations to allow. 196 | * 197 | * @param int $times The maximum iterations of invocations to allow 198 | * 199 | * @return $this 200 | */ 201 | public function onlyXtimes($times) 202 | { 203 | $this->maxIterations = (int) $times; 204 | 205 | return $this; 206 | } 207 | 208 | /** 209 | * Prevent the callback from being triggered again. 210 | * 211 | * @return $this 212 | */ 213 | public function bypass() 214 | { 215 | $this->onlyXtimes(0); 216 | 217 | return $this; 218 | } 219 | 220 | /** 221 | * Set the priority the callback should be registered with. 222 | * 223 | * @param mixed $priority The callback priority 224 | * 225 | * @return $this 226 | */ 227 | public function withPriority($priority) 228 | { 229 | $this->remove(); 230 | 231 | $this->priority = $priority; 232 | 233 | $this->listen(); 234 | 235 | return $this; 236 | } 237 | 238 | /** 239 | * Add a condition to control the invocation of the callback. 240 | * 241 | * @param callable $condition A function to evaluate a condition before the 242 | * hook's callback is invoked. 243 | * If the function returns false, the callback 244 | * will not be invoked. 245 | * 246 | * @return $this 247 | */ 248 | public function onlyIf(callable $condition) 249 | { 250 | $this->conditions()->push(new Callback($condition)); 251 | 252 | return $this; 253 | } 254 | 255 | /** 256 | * Get the collection of callback invocation conditions. 257 | * 258 | * @return Collection 259 | */ 260 | protected function conditions() 261 | { 262 | if (is_null($this->conditions)) { 263 | $this->conditions = new Collection; 264 | } 265 | 266 | return $this->conditions; 267 | } 268 | 269 | /** 270 | * Whether or not the callback has reached the limit of allowed invocations. 271 | * 272 | * @return boolean true for limit reached/exceeded, otherwise false 273 | */ 274 | protected function hasExceededIterations() 275 | { 276 | return ($this->maxIterations > -1) && ($this->iterations >= $this->maxIterations); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/Exception/WP_ErrorException.php: -------------------------------------------------------------------------------- 1 | message = $error->get_error_message(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Meta/Meta.php: -------------------------------------------------------------------------------- 1 | type = $type; 37 | $this->object_id = (int) $object_id; 38 | $this->key = $key; 39 | } 40 | 41 | /** 42 | * Get the single metadata. 43 | * 44 | * @return mixed 45 | */ 46 | public function get() 47 | { 48 | return get_metadata($this->type, $this->object_id, $this->key, true); 49 | } 50 | 51 | /** 52 | * Get all metadata as a Collection. 53 | * 54 | * @return Collection 55 | */ 56 | public function collect() 57 | { 58 | return new Collection($this->all()); 59 | } 60 | 61 | /** 62 | * Get all metadata as an array. 63 | * 64 | * @return array 65 | */ 66 | public function all() 67 | { 68 | return (array) get_metadata($this->type, $this->object_id, $this->key, false); 69 | } 70 | 71 | /** 72 | * Set the new value. 73 | * 74 | * @param mixed $value 75 | * @param string $prev_value 76 | * 77 | * @return $this 78 | */ 79 | public function set($value, $prev_value = '') 80 | { 81 | update_metadata($this->type, $this->object_id, $this->key, $value, $prev_value); 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Replace a single value. 88 | * 89 | * @param mixed $old Previous value to update 90 | * @param mixed $new New value to set the previous value to 91 | * 92 | * @return $this 93 | */ 94 | public function replace($old, $new) 95 | { 96 | return $this->set($new, $old); 97 | } 98 | 99 | /** 100 | * Add metadata for the specified object. 101 | * 102 | * @param mixed $value The value to add 103 | * @param bool $unique Whether the specified metadata key should be unique 104 | * for the object. If true, and the object already has 105 | * a value for the specified metadata key, no change will be made. 106 | * 107 | * @return $this 108 | */ 109 | public function add($value, $unique = false) 110 | { 111 | add_metadata($this->type, $this->object_id, $this->key, $value, $unique); 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * Delete the metadata 118 | * 119 | * Deletes all metadata for the key, if provided, optionally filtered by 120 | * a previous value. 121 | * If no key was provided, all metadata for the object is deleted. 122 | * 123 | * @param string $value The old value to delete. 124 | * This is only necessary when deleting a specific value 125 | * from an object which has multiple values for the key. 126 | * 127 | * @return $this 128 | */ 129 | public function delete($value = '') 130 | { 131 | delete_metadata($this->type, $this->object_id, $this->key, $value); 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * Determine if a meta key is set for a given object. 138 | * 139 | * @return bool True of the key is set, false if not. 140 | */ 141 | public function exists() 142 | { 143 | return metadata_exists($this->type, $this->object_id, $this->key); 144 | } 145 | 146 | /** 147 | * Get the metadata as a string. 148 | * 149 | * @return string The meta value 150 | */ 151 | public function __toString() 152 | { 153 | return $this->get(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Meta/ObjectMeta.php: -------------------------------------------------------------------------------- 1 | type = $type; 34 | $this->id = (int) $id; 35 | } 36 | 37 | /** 38 | * Get meta object for the key. 39 | * 40 | * @param string $key meta key 41 | * 42 | * @return Meta 43 | */ 44 | public function get($key) 45 | { 46 | return new Meta($this->type, $this->id, $key); 47 | } 48 | 49 | /** 50 | * Set the value for the given key. 51 | * 52 | * @param string $key Meta key 53 | * @param mixed $value New meta value 54 | * 55 | * @return $this 56 | */ 57 | public function set($key, $value) 58 | { 59 | $this->get($key)->set($value); 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * Get all meta for the object as a Collection. 66 | * 67 | * @return Collection 68 | */ 69 | public function collect() 70 | { 71 | return Collection::make($this->toArray())->map(function ($value, $key) { 72 | return new Meta($this->type, $this->id, $key); 73 | }); 74 | } 75 | 76 | /** 77 | * Get the representation of the instance as an array. 78 | * 79 | * @return array 80 | */ 81 | public function toArray() 82 | { 83 | return (array) get_metadata($this->type, $this->id, '', false); 84 | } 85 | 86 | /** 87 | * Magic Getter. 88 | * 89 | * @param string $property Accessed property 90 | * 91 | * @return mixed 92 | */ 93 | public function __get($property) 94 | { 95 | return isset($this->$property) ? $this->$property : null; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Post/ClassNameAsPostType.php: -------------------------------------------------------------------------------- 1 | getShortName(); 47 | 48 | /** 49 | * Adapted from Str::snake() 50 | * @link https://github.com/laravel/framework/blob/5.2/src/Illuminate/Support/Str.php 51 | */ 52 | if (! ctype_lower($name)) { 53 | $name = strtolower(preg_replace('/(.)(?=[A-Z])/u', '$1_', $name)); 54 | } 55 | 56 | return static::$classNamePostType = $name; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Post/Exception/ModelPostTypeMismatchException.php: -------------------------------------------------------------------------------- 1 | modelClass = $modelClass; 32 | $this->post = $post; 33 | $this->message = str_replace([ 34 | '{modelClass}', 35 | '{givenPostType}', 36 | '{modelPostType}' 37 | ], [ 38 | $this->modelClass, 39 | $this->post->post_type, 40 | call_user_func([$this->modelClass, 'postTypeId']) 41 | ], static::MESSAGE_FORMAT); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Post/Exception/PostNotFoundException.php: -------------------------------------------------------------------------------- 1 | post_type = static::postTypeId(); 75 | } elseif ($post->post_type !== static::postTypeId()) { 76 | throw new ModelPostTypeMismatchException(static::class, $post); 77 | } 78 | 79 | $this->setObject($post); 80 | 81 | $this->fill($attributes); 82 | } 83 | 84 | /** 85 | * Retrieve a new instance by the ID. 86 | * 87 | * @param int|string $id Primary ID 88 | * 89 | * @return null|static 90 | */ 91 | public static function find($id) 92 | { 93 | try { 94 | return static::fromID($id); 95 | } catch (\Exception $e) { 96 | return null; 97 | } 98 | } 99 | 100 | /** 101 | * Create a new instance from a Post with the given ID 102 | * 103 | * @param int|string $id Post ID of post to create the instance from 104 | * 105 | * @return static 106 | */ 107 | public static function fromID($id) 108 | { 109 | $post = WP_Post::get_instance($id); 110 | 111 | if (false === $post) { 112 | throw new PostNotFoundException("No post found with ID {$id}"); 113 | } 114 | 115 | return new static($post); 116 | } 117 | 118 | /** 119 | * Create a new instance from a Post with the given slug 120 | * 121 | * @param string $slug the post slug 122 | * 123 | * @return static 124 | */ 125 | public static function fromSlug($slug) 126 | { 127 | $found = static::whereSlug($slug)->limit(1)->results(); 128 | 129 | if ($found->isEmpty()) { 130 | throw new PostNotFoundException("No post found with slug {$slug}"); 131 | } 132 | 133 | return $found->first(); 134 | } 135 | 136 | /** 137 | * Create a new instance from the global $post 138 | * 139 | * @return static 140 | */ 141 | public static function fromGlobal() 142 | { 143 | if (! $GLOBALS['post'] instanceof WP_Post) { 144 | throw new PostNotFoundException('Global $post not an instance of WP_Post'); 145 | } 146 | 147 | return new static($GLOBALS['post']); 148 | } 149 | 150 | /** 151 | * Get the post type identifier for this model 152 | * 153 | * @return string post type identifier (slug) 154 | */ 155 | public static function postTypeId() 156 | { 157 | return static::POST_TYPE; 158 | } 159 | 160 | /** 161 | * Get the post type API 162 | * 163 | * @return mixed Loads an existing type as a new PostType, 164 | * or returns a new PostTypeBuilder for registering a new type. 165 | */ 166 | public static function postType() 167 | { 168 | return PostType::make(static::postTypeId()); 169 | } 170 | 171 | /** 172 | * Get the permalink URL. 173 | * 174 | * @return string|bool The permalink URL, or false if the post does not exist. 175 | */ 176 | public function url() 177 | { 178 | return get_permalink($this->id); 179 | } 180 | 181 | /** 182 | * Send the post to the trash 183 | * 184 | * If trash is disabled, the post or page is permanently deleted. 185 | * 186 | * @return $this 187 | */ 188 | public function trash() 189 | { 190 | if (wp_trash_post($this->id)) { 191 | $this->refresh(); 192 | } 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * Restore a post or page from the Trash 199 | * 200 | * @return $this 201 | */ 202 | public function untrash() 203 | { 204 | if (wp_untrash_post($this->id)) { 205 | $this->refresh(); 206 | } 207 | 208 | return $this; 209 | } 210 | 211 | /** 212 | * Get a new query builder for the model. 213 | * 214 | * @return QueryBuilder 215 | */ 216 | public function newQuery() 217 | { 218 | return QueryBuilder::make()->setModel($this); 219 | } 220 | 221 | /** 222 | * Save the post to the database. 223 | * 224 | * @throws WP_ErrorException 225 | * 226 | * @return $this 227 | */ 228 | public function save() 229 | { 230 | if (! $this->id) { 231 | $result = wp_insert_post($this->object->to_array(), true); 232 | } else { 233 | $result = wp_update_post($this->object, true); 234 | } 235 | 236 | if (is_wp_error($result)) { 237 | throw new WP_ErrorException($result); 238 | } 239 | 240 | $this->setId($result)->refresh(); 241 | 242 | return $this; 243 | } 244 | 245 | /** 246 | * Permanently delete the post from the database. 247 | * 248 | * @return $this 249 | */ 250 | public function delete() 251 | { 252 | if (wp_delete_post($this->id, true)) { 253 | $this->refresh(); 254 | } 255 | 256 | return $this; 257 | } 258 | 259 | /** 260 | * Update the modeled object with the current state from the database. 261 | * 262 | * @return $this 263 | */ 264 | public function refresh() 265 | { 266 | $this->setObject(WP_Post::get_instance($this->id)); 267 | 268 | return $this; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/Post/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | query = $query; 32 | } 33 | 34 | /** 35 | * Create a new instance. 36 | * 37 | * @param WP_Query $query 38 | * 39 | * @return static 40 | */ 41 | public static function make(WP_Query $query = null) 42 | { 43 | return new static($query); 44 | } 45 | 46 | /** 47 | * Limit the number of returned results 48 | * 49 | * @param integer $limit The maximum number of results to return 50 | * use -1 for no limit 51 | * 52 | * @return $this 53 | */ 54 | public function limit($limit) 55 | { 56 | return $this->set('posts_per_page', (int) $limit); 57 | } 58 | 59 | /** 60 | * Return an unlimited number of results. 61 | * 62 | * @return $this 63 | */ 64 | public function all() 65 | { 66 | return $this->limit(-1); 67 | } 68 | 69 | /** 70 | * Set the order for the query 71 | * 72 | * @param string $order 73 | * 74 | * @return $this 75 | */ 76 | public function order($order) 77 | { 78 | return $this->set('order', strtoupper($order)); 79 | } 80 | 81 | /** 82 | * Query by post status 83 | * 84 | * @param string|array $status the post status or stati to match 85 | * 86 | * @return $this 87 | */ 88 | public function whereStatus($status) 89 | { 90 | return $this->set('post_status', $status); 91 | } 92 | 93 | /** 94 | * Query by slug 95 | * 96 | * @param string $slug the post slug to query by 97 | * 98 | * @return $this 99 | */ 100 | public function whereSlug($slug) 101 | { 102 | return $this->set('name', $slug); 103 | } 104 | 105 | /** 106 | * Set a query variable on the query 107 | * 108 | * @param string $var Query variable key 109 | * @param mixed $value Query value for key 110 | * 111 | * @return $this 112 | */ 113 | public function set($var, $value) 114 | { 115 | $this->query->set($var, $value); 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * Execute the query and return the raw results. 122 | * 123 | * @return array 124 | */ 125 | protected function query() 126 | { 127 | if ($this->model) { 128 | $this->set('post_type', $this->model->post_type) 129 | ->set('fields', ''); // as WP_Post objects 130 | } 131 | 132 | return $this->query->get_posts(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/PostType/Builder.php: -------------------------------------------------------------------------------- 1 | 'Add New {one}', 16 | 'all_items' => 'All {many}', 17 | 'archives' => '{one} Archives', 18 | 'edit_item' => 'Edit {one}', 19 | 'filter_items_list' => 'Filter {many} list', 20 | 'insert_into_item' => 'Insert into {one}', 21 | 'items_list_navigation' => '{many} list navigation', 22 | 'items_list' => '{many} list', 23 | 'menu_name' => '{many}', 24 | 'name_admin_bar' => '{one}', 25 | 'name' => '{many}', 26 | 'new_item' => 'New {one}', 27 | 'not_found_in_trash' => 'No {many} found in Trash.', 28 | 'not_found' => 'No {many} found.', 29 | 'search_items' => 'Search {many}', 30 | 'singular_name' => '{one}', 31 | 'uploaded_to_this_item' => 'Uploaded to this {one}', 32 | 'view_item' => 'View {one}', 33 | ]; 34 | 35 | /** 36 | * Specify which features the post type supports. 37 | * 38 | * @param mixed $features array of features 39 | * string ...$features features as parameters 40 | * 41 | * @return $this 42 | */ 43 | public function supports($features) 44 | { 45 | if (! is_array($features)) { 46 | $features = func_get_args(); 47 | } 48 | 49 | return $this->set('supports', $features); 50 | } 51 | 52 | /** 53 | * Set the post type as publicly available. 54 | * 55 | * @return $this 56 | */ 57 | public function open() 58 | { 59 | return $this->set('public', true); 60 | } 61 | 62 | /** 63 | * Set the post type as non-publicly available. 64 | * 65 | * @return $this 66 | */ 67 | public function closed() 68 | { 69 | return $this->set('public', false); 70 | } 71 | 72 | /** 73 | * Enable admin interface. 74 | * 75 | * @return $this 76 | */ 77 | public function withUI() 78 | { 79 | return $this->set('show_ui', true); 80 | } 81 | 82 | /** 83 | * Disable admin interface. 84 | * 85 | * @return $this 86 | */ 87 | public function noUI() 88 | { 89 | return $this->set('show_ui', false); 90 | } 91 | 92 | /** 93 | * Register the post type. 94 | * 95 | * @return PostType 96 | */ 97 | public function register() 98 | { 99 | if (! $this->id || strlen($this->id) > 20) { 100 | throw new InvalidPostTypeNameException('Post type names must be between 1 and 20 characters in length.'); 101 | } 102 | 103 | $object = register_post_type($this->id, $this->assembleArgs()); 104 | 105 | return new PostType($object); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/PostType/Exception/InvalidPostTypeNameException.php: -------------------------------------------------------------------------------- 1 | object = $object; 28 | } 29 | 30 | /** 31 | * Create a new instance using the post type slug. 32 | * 33 | * Loads an existing type, or returns a new builder for registering a new type. 34 | * 35 | * @param string $id The post type identifier 36 | * 37 | * @return static|Builder If the post type has been registered, a new static instance is returned. 38 | * Otherwise a new Builder is created for building a new post type to register. 39 | */ 40 | public static function make($id) 41 | { 42 | if (static::exists($id)) { 43 | return static::load($id); 44 | } 45 | 46 | return static::build($id); 47 | } 48 | 49 | /** 50 | * Create a new instance from an existing type. 51 | * 52 | * @param string $id The post type identifier 53 | * 54 | * @return static 55 | */ 56 | public static function load($id) 57 | { 58 | if (! $object = get_post_type_object($id)) { 59 | throw new NonExistentPostTypeException("No post type exists with name '$id'."); 60 | } 61 | 62 | return new static($object); 63 | } 64 | 65 | 66 | /** 67 | * Build a new type to be registered. 68 | * 69 | * @param $id 70 | * 71 | * @return mixed 72 | */ 73 | public static function build($id) 74 | { 75 | return new Builder($id); 76 | } 77 | 78 | /** 79 | * Get the post type identifier (aka: name/slug). 80 | */ 81 | public function id() 82 | { 83 | return $this->object->name; 84 | } 85 | 86 | /** 87 | * Checks if a post type with this slug has been registered. 88 | * 89 | * @param string $id The post type identifier 90 | * 91 | * @return bool 92 | */ 93 | public static function exists($id) 94 | { 95 | return post_type_exists($id); 96 | } 97 | 98 | /** 99 | * Check for feature support. 100 | * 101 | * @param string|array $features string - First feature of possible many, 102 | * array - Many features to check support for. 103 | * 104 | * @return mixed 105 | */ 106 | public function supports($features) 107 | { 108 | if (! is_array($features)) { 109 | $features = func_get_args(); 110 | } 111 | 112 | return ! Collection::make($features) 113 | ->contains(function ($feature) { 114 | return ! post_type_supports($this->id(), $feature); 115 | }); 116 | } 117 | 118 | /** 119 | * Register support of certain features for an existing post type. 120 | * 121 | * @param mixed $features string - single feature to add 122 | * array - multiple features to add 123 | * 124 | * @return $this 125 | */ 126 | public function addSupportFor($features) 127 | { 128 | add_post_type_support($this->id(), is_array($features) ? $features : func_get_args()); 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Un-register support of certain features for an existing post type. 135 | * 136 | * @param mixed $features string - single feature to remove 137 | * array - multiple features to remove 138 | * 139 | * @return $this 140 | */ 141 | public function removeSupportFor($features) 142 | { 143 | Collection::make(is_array($features) ? $features : func_get_args()) 144 | ->each(function ($features) { 145 | remove_post_type_support($this->id(), $features); 146 | }); 147 | 148 | return $this; 149 | } 150 | 151 | /** 152 | * Unregister the post type. 153 | * 154 | * @throws NonExistentPostTypeException 155 | * @throws WP_ErrorException 156 | * 157 | * @return $this 158 | */ 159 | public function unregister() 160 | { 161 | $id = $this->id(); 162 | 163 | if (! static::exists($id)) { 164 | throw new NonExistentPostTypeException("No post type exists with name '{$id}'."); 165 | } 166 | 167 | if (is_wp_error($error = unregister_post_type($id))) { 168 | throw new WP_ErrorException($error); 169 | } 170 | 171 | return $this; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Query/Builder.php: -------------------------------------------------------------------------------- 1 | model) { 36 | return $this->collectModels(); 37 | } 38 | 39 | return new Collection($this->query()); 40 | } 41 | 42 | /** 43 | * Get the results as a collection of model instances. 44 | * 45 | * @return Collection 46 | */ 47 | protected function collectModels() 48 | { 49 | $modelClass = get_class($this->model); 50 | 51 | return Collection::make($this->query()) 52 | ->map(function ($result) use ($modelClass) { 53 | return new $modelClass($result); 54 | }); 55 | } 56 | 57 | /** 58 | * Set the model for this query. 59 | * 60 | * @param Model $model 61 | * 62 | * @return $this 63 | */ 64 | public function setModel(Model $model) 65 | { 66 | $this->model = $model; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Get the model 73 | * 74 | * @return Model 75 | */ 76 | public function getModel() 77 | { 78 | return $this->model; 79 | } 80 | 81 | /** 82 | * Get the query object. 83 | * 84 | * @return object 85 | */ 86 | public function getQuery() 87 | { 88 | return $this->query; 89 | } 90 | 91 | /** 92 | * Handle dynamic method calls on the builder. 93 | * 94 | * @param string $name Method name 95 | * @param array $arguments 96 | * 97 | * @return mixed 98 | */ 99 | public function __call($name, $arguments) 100 | { 101 | if (method_exists($this->model, 'scope' . ucfirst($name))) { 102 | return $this->model->{'scope' . ucfirst($name)}($this, ...$arguments); 103 | } 104 | 105 | throw new \BadMethodCallException("No '$name' method exists on " . static::class); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Silk.php: -------------------------------------------------------------------------------- 1 | target = static::normalizeSyntax($target); 21 | } 22 | 23 | /** 24 | * Normalize the callable syntax 25 | * 26 | * Converts string static class method to standard callable array. 27 | * 28 | * @param mixed $callback Target callback 29 | * 30 | * @return mixed Closure Anonymous function 31 | * array Class method 32 | * string Function 33 | */ 34 | public static function normalizeSyntax(callable $callback) 35 | { 36 | if (is_string($callback) && false !== strpos($callback, '::')) { 37 | $callback = explode('::', $callback); 38 | } 39 | 40 | return $callback; 41 | } 42 | 43 | /** 44 | * Call the target callable 45 | * 46 | * @return mixed Returns the return value of the callback, or FALSE on error. 47 | */ 48 | public function call() 49 | { 50 | return $this->callArray(func_get_args()); 51 | } 52 | 53 | /** 54 | * Call the target callable, with an array of arguments 55 | * 56 | * @param array $arguments The parameters to be passed to the callback, as an indexed array. 57 | * @return mixed Returns the return value of the callback, or FALSE on error. 58 | */ 59 | public function callArray(array $arguments = []) 60 | { 61 | return call_user_func_array($this->target, $arguments); 62 | } 63 | 64 | /** 65 | * Get the target callable 66 | * 67 | * @return mixed The normalized callable 68 | */ 69 | public function get() 70 | { 71 | return $this->target; 72 | } 73 | 74 | /** 75 | * Get the number of parameters from the callback's signature 76 | * 77 | * @return int 78 | */ 79 | public function parameterCount() 80 | { 81 | return $this->reflect()->getNumberOfParameters(); 82 | } 83 | 84 | /** 85 | * Get the corresponding Reflection instance for the target callable 86 | * 87 | * @return \ReflectionFunctionAbstract 88 | */ 89 | public function reflect() 90 | { 91 | if ($this->target instanceof \Closure 92 | || (is_string($this->target) && function_exists($this->target)) 93 | ) { 94 | return new \ReflectionFunction($this->target); 95 | } 96 | 97 | list($class, $method) = $this->target; 98 | 99 | return new \ReflectionMethod($class, $method); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Support/Collection.php: -------------------------------------------------------------------------------- 1 | attributes = $atts; 35 | $this->content = $content; 36 | $this->tag = $tag; 37 | } 38 | 39 | /** 40 | * Register a tag for this shortcode. 41 | * 42 | * @param mixed $tag The tag to register with this shortcode class 43 | */ 44 | public static function register($tag) 45 | { 46 | add_shortcode((string) $tag, [static::class, 'controller']); 47 | } 48 | 49 | /** 50 | * WordPress Shortcode Callback 51 | * 52 | * @param mixed $atts Shortcode attributes 53 | * @param string $content The inner (enclosed) content 54 | * @param string $tag The called shortcode tag 55 | * 56 | * @return static 57 | */ 58 | public static function controller($atts, $content, $tag) 59 | { 60 | return (new static((array) $atts, $content, $tag))->render(); 61 | } 62 | 63 | /** 64 | * Call the shortcode's handler and return the output. 65 | * 66 | * @return mixed Rendered shortcode output 67 | */ 68 | public function render() 69 | { 70 | $dedicated_method = "{$this->tag}_handler"; 71 | 72 | if (method_exists($this, $dedicated_method)) { 73 | return $this->$dedicated_method(); 74 | } 75 | 76 | return $this->handler(); 77 | } 78 | 79 | /** 80 | * Catch-all render method. 81 | * 82 | * @return string 83 | */ 84 | protected function handler() 85 | { 86 | return ''; // Override this in a sub-class 87 | } 88 | 89 | /** 90 | * Get all attributes as a collection. 91 | * 92 | * @return Collection 93 | */ 94 | public function attributes() 95 | { 96 | return new Collection($this->attributes); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Taxonomy/Builder.php: -------------------------------------------------------------------------------- 1 | 'Add New {one}', 22 | 'add_or_remove_items' => null, 23 | 'all_items' => 'All {many}', 24 | 'archives' => 'All {many}', 25 | 'choose_from_most_used' => null, 26 | 'edit_item' => 'Edit {one}', 27 | 'items_list_navigation' => '{many} list navigation', 28 | 'items_list' => '{many} list', 29 | 'menu_name' => '{many}', 30 | 'name_admin_bar' => '{one}', 31 | 'name' => '{many}', 32 | 'new_item_name' => 'New {one} Name', 33 | 'no_terms' => 'No {many}', 34 | 'not_found' => 'No {many} found.', 35 | 'parent_item_colon' => 'Parent {one}:', 36 | 'parent_item' => 'Parent {one}', 37 | 'popular_items' => null, 38 | 'search_items' => 'Search {many}', 39 | 'separate_items_with_commas' => null, 40 | 'singular_name' => '{one}', 41 | 'update_item' => 'Update {one}', 42 | 'view_item' => 'View {one}', 43 | ]; 44 | 45 | /** 46 | * Specify which object types the taxonomy is for. 47 | * 48 | * @param string|array $types A list of object types or an array. 49 | * 50 | * @return $this 51 | */ 52 | public function forTypes($types) 53 | { 54 | $this->objectTypes = is_array($types) ? $types : func_get_args(); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Register and return the new taxonomy. 61 | * 62 | * @throws InvalidTaxonomyNameException 63 | * 64 | * @return Taxonomy 65 | */ 66 | public function register() 67 | { 68 | if (! $this->id || strlen($this->id) > 32) { 69 | throw new InvalidTaxonomyNameException('Taxonomy names must be between 1 and 32 characters in length.'); 70 | } 71 | 72 | register_taxonomy($this->id, $this->objectTypes, $this->assembleArgs()); 73 | 74 | return Taxonomy::load($this->id); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Taxonomy/Exception/InvalidTaxonomyNameException.php: -------------------------------------------------------------------------------- 1 | name) || ! static::exists($taxonomy->name)) { 49 | throw new NonExistentTaxonomyException; 50 | } 51 | 52 | $this->object = $taxonomy; 53 | } 54 | 55 | /** 56 | * Create a new instance using the taxonomy identifier. 57 | * 58 | * @param string $id Taxonomy name/identifier 59 | * 60 | * @throws NonExistentTaxonomyException 61 | * @throws InvalidTaxonomyNameException 62 | * 63 | * @return static|Builder 64 | */ 65 | public static function make($id) 66 | { 67 | if (static::exists($id)) { 68 | return static::load($id); 69 | } 70 | 71 | if (! $id || strlen($id) > 32) { 72 | throw new InvalidTaxonomyNameException('Taxonomy names must be between 1 and 32 characters in length.'); 73 | } 74 | 75 | return static::build($id); 76 | } 77 | 78 | /** 79 | * Create a new instance from an existing taxonomy. 80 | * 81 | * @param string $id The taxonomy identifier 82 | * 83 | * @throws NonExistentTaxonomyException 84 | * 85 | * @return static 86 | */ 87 | public static function load($id) 88 | { 89 | if (! $object = get_taxonomy($id)) { 90 | throw new NonExistentTaxonomyException("No taxonomy exists with name '$id'."); 91 | } 92 | 93 | return new static($object); 94 | } 95 | 96 | /** 97 | * Build a new Taxonomy to be registered. 98 | * 99 | * @param $id 100 | * 101 | * @return Builder 102 | */ 103 | public static function build($id) 104 | { 105 | return new Builder($id); 106 | } 107 | 108 | /** 109 | * Check if a Taxonomy exists for the given identifier. 110 | * 111 | * @param string $id The taxonomy key/identifier 112 | * 113 | * @return bool 114 | */ 115 | public static function exists($id) 116 | { 117 | return taxonomy_exists($id); 118 | } 119 | 120 | /** 121 | * @return mixed 122 | */ 123 | public function id() 124 | { 125 | return $this->object->name; 126 | } 127 | 128 | /** 129 | * Start a new query for terms of this taxonomy. 130 | * 131 | * @return QueryBuilder 132 | */ 133 | public function terms() 134 | { 135 | return (new QueryBuilder)->forTaxonomy($this->id()); 136 | } 137 | 138 | /** 139 | * Get all post types associated with this taxonomy. 140 | * 141 | * @return Collection 142 | */ 143 | public function postTypes() 144 | { 145 | return Collection::make($this->object_type) 146 | ->map(function ($post_type) { 147 | return PostType::load($post_type); 148 | }); 149 | } 150 | 151 | /** 152 | * Unregister the taxonomy. 153 | * 154 | * @throws NonExistentTaxonomyException 155 | * @throws WP_ErrorException 156 | * 157 | * @return $this 158 | */ 159 | public function unregister() 160 | { 161 | if (! $this->exists($this->id())) { 162 | throw new NonExistentTaxonomyException; 163 | } 164 | 165 | if (is_wp_error($error = unregister_taxonomy($this->id()))) { 166 | throw new WP_ErrorException($error); 167 | } 168 | 169 | return $this; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Term/Exception/TaxonomyMismatchException.php: -------------------------------------------------------------------------------- 1 | taxonomy = static::TAXONOMY; 59 | } elseif ($term->taxonomy != static::TAXONOMY) { 60 | throw new TaxonomyMismatchException(); 61 | } 62 | 63 | $this->setObject($term); 64 | 65 | $this->fill($attributes); 66 | } 67 | 68 | /** 69 | * Retrieve a new instance by the ID. 70 | * 71 | * @param int|string $id Primary ID 72 | * 73 | * @return null|static 74 | */ 75 | public static function find($id) 76 | { 77 | try { 78 | return static::fromID($id); 79 | } catch (\Exception $e) { 80 | return null; 81 | } 82 | } 83 | 84 | /** 85 | * Create a new instance from a term ID. 86 | * 87 | * @param int|string $id Term ID 88 | * 89 | * @throws TermNotFoundException 90 | * 91 | * @return static 92 | */ 93 | public static function fromID($id) 94 | { 95 | if (! $term = WP_Term::get_instance($id, static::TAXONOMY)) { 96 | throw new TermNotFoundException("No term found with ID $id."); 97 | } 98 | 99 | return new static($term); 100 | } 101 | 102 | /** 103 | * Create a new instance from a slug. 104 | * 105 | * @param string $slug Term slug 106 | * 107 | * @throws TermNotFoundException 108 | * 109 | * @return static 110 | */ 111 | public static function fromSlug($slug) 112 | { 113 | if (! $term = get_term_by('slug', $slug, static::TAXONOMY)) { 114 | throw new TermNotFoundException("No term found with slug '$slug'."); 115 | } 116 | 117 | return new static($term); 118 | } 119 | 120 | /** 121 | * Check if this term exists in the database. 122 | * 123 | * @return boolean 124 | */ 125 | public function exists() 126 | { 127 | return $this->id && ((bool) term_exists((int) $this->id, static::TAXONOMY)); 128 | } 129 | 130 | /** 131 | * Check if this term exists in the database as the child of the given parent. 132 | * 133 | * @param int|string|object $parent integer Parent term ID 134 | * string Parent term slug or name 135 | * object The parent term object/model. 136 | * 137 | * @return boolean True if the this term and the parent 138 | * exist in the database, and the instance 139 | * is a child of the given parent; 140 | * otherwise false 141 | */ 142 | public function isChildOf($parent) 143 | { 144 | if (isset($parent->term_id)) { 145 | $parent = $parent->term_id; 146 | } 147 | 148 | return (bool) term_exists((int) $this->id, static::TAXONOMY, $parent); 149 | } 150 | 151 | /** 152 | * Get the parent term instance. 153 | * 154 | * @return static 155 | */ 156 | public function parent() 157 | { 158 | return static::fromID($this->object->parent); 159 | } 160 | 161 | /** 162 | * Get all ancestors of this term as a collection. 163 | * 164 | * @return Collection 165 | */ 166 | public function ancestors() 167 | { 168 | return Collection::make(get_ancestors($this->id, static::TAXONOMY, 'taxonomy')) 169 | ->map([static::class, 'fromID']); 170 | } 171 | 172 | /** 173 | * Get all children of this term as a collection. 174 | * 175 | * @return Collection 176 | */ 177 | public function children() 178 | { 179 | return Collection::make(get_term_children($this->id, static::TAXONOMY)) 180 | ->map([static::class, 'fromID']); 181 | } 182 | 183 | /** 184 | * Get the Taxonomy model. 185 | * 186 | * @return Taxonomy|\Silk\Taxonomy\Builder 187 | */ 188 | public static function taxonomy() 189 | { 190 | return Taxonomy::make(static::TAXONOMY); 191 | } 192 | 193 | /** 194 | * Get the URL for this term. 195 | * 196 | * @return string|bool 197 | */ 198 | public function url() 199 | { 200 | $url = get_term_link($this->id, $this->taxonomy); 201 | 202 | if (is_wp_error($url)) { 203 | throw new WP_ErrorException($url); 204 | } 205 | 206 | return $url; 207 | } 208 | 209 | /** 210 | * Start a new query for terms of this type. 211 | * 212 | * @return QueryBuilder 213 | */ 214 | public function newQuery() 215 | { 216 | return QueryBuilder::make()->setModel($this); 217 | } 218 | 219 | /** 220 | * Save the term to the database. 221 | * 222 | * @throws WP_ErrorException 223 | * 224 | * @return $this 225 | */ 226 | public function save() 227 | { 228 | if ($this->id) { 229 | $ids = wp_update_term($this->id, $this->taxonomy, $this->object->to_array()); 230 | } else { 231 | $ids = wp_insert_term($this->name, $this->taxonomy, $this->object->to_array()); 232 | } 233 | 234 | if (is_wp_error($ids)) { 235 | throw new WP_ErrorException($ids); 236 | } 237 | 238 | $this->setId($ids['term_id'])->refresh(); 239 | 240 | return $this; 241 | } 242 | 243 | /** 244 | * Delete the term from the database. 245 | * 246 | * @return $this 247 | */ 248 | public function delete() 249 | { 250 | if (wp_delete_term($this->id, $this->taxonomy)) { 251 | $this->setObject(new WP_Term(new stdClass)); 252 | } 253 | 254 | return $this; 255 | } 256 | 257 | /** 258 | * Reload the term object from the database. 259 | * 260 | * @return $this 261 | */ 262 | public function refresh() 263 | { 264 | $this->setObject(WP_Term::get_instance($this->id, $this->taxonomy)); 265 | 266 | return $this; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/Term/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | query = new Collection($args); 34 | } 35 | 36 | /** 37 | * Create a new instance. 38 | * 39 | * @return static 40 | */ 41 | public static function make() 42 | { 43 | return new static; 44 | } 45 | 46 | /** 47 | * Restrict the query to terms of the provided Taxonomy. 48 | * 49 | * @param string $taxonomy 50 | * 51 | * @return $this 52 | */ 53 | public function forTaxonomy($taxonomy) 54 | { 55 | $this->taxonomy = $taxonomy; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Get all terms. 62 | * 63 | * @return $this 64 | */ 65 | public function all() 66 | { 67 | return $this->includeEmpty() 68 | ->limit('all'); 69 | } 70 | 71 | /** 72 | * Include terms that have no related objects in the results. 73 | * 74 | * @return $this 75 | */ 76 | public function includeEmpty() 77 | { 78 | return $this->set('hide_empty', false); 79 | } 80 | 81 | /** 82 | * Limit the maximum number of results returned. 83 | * 84 | * @param int $max_results Maximum number to return. 0 or 'all' for unlimited. 85 | * 86 | * @return $this 87 | */ 88 | public function limit($max_results) 89 | { 90 | return $this->set('number', intval($max_results)); 91 | } 92 | 93 | /** 94 | * Execute the query and return the raw results. 95 | * 96 | * @throws WP_ErrorException 97 | * 98 | * @return array 99 | */ 100 | protected function query() 101 | { 102 | if ($this->model) { 103 | $this->set('taxonomy', $this->model->taxonomy) 104 | ->set('fields', 'all'); 105 | } elseif ($this->taxonomy) { 106 | $this->set('taxonomy', $this->taxonomy); 107 | } 108 | 109 | if (is_wp_error($terms = get_terms($this->query->toArray()))) { 110 | throw new WP_ErrorException($terms); 111 | } 112 | 113 | return $terms; 114 | } 115 | 116 | /** 117 | * Set an arbitrary query parameter. 118 | * 119 | * @param $parameter 120 | * @param $value 121 | * 122 | * @return $this 123 | */ 124 | public function set($parameter, $value) 125 | { 126 | $this->query->put($parameter, $value); 127 | 128 | return $this; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Type/Builder.php: -------------------------------------------------------------------------------- 1 | id = $id; 42 | $this->args = new Collection($args); 43 | } 44 | 45 | /** 46 | * Create a new instance. 47 | * 48 | * @param string $type 49 | * 50 | * @return static 51 | */ 52 | public static function make($type) 53 | { 54 | return new static($type); 55 | } 56 | 57 | /** 58 | * Register the type. 59 | * 60 | * @return mixed 61 | */ 62 | abstract public function register(); 63 | 64 | /** 65 | * Assemble the arguments for registration. 66 | * 67 | * @return array 68 | */ 69 | protected function assembleArgs() 70 | { 71 | return $this->args->put('labels', $this->labels())->toArray(); 72 | } 73 | 74 | /** 75 | * Set the singular label for this post type. 76 | * 77 | * @param string $singular_label 78 | * 79 | * @return $this 80 | */ 81 | public function oneIs($singular_label) 82 | { 83 | $this->labels()->setSingular($singular_label); 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * Set the plural label for this post type. 90 | * 91 | * @param string $plural_label 92 | * 93 | * @return $this 94 | */ 95 | public function manyAre($plural_label) 96 | { 97 | $this->labels()->setPlural($plural_label); 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * Get the labels instance. 104 | * 105 | * @return Labels 106 | */ 107 | protected function labels() 108 | { 109 | if (! $this->labels) { 110 | $this->labels = Labels::make( 111 | $this->labelDefaults 112 | )->merge($this->args->get('labels', [])); 113 | } 114 | 115 | return $this->labels; 116 | } 117 | 118 | /** 119 | * Set a label for the given key. 120 | * 121 | * @param string $key Label key 122 | * @param string $value Label value 123 | * 124 | * @return $this 125 | */ 126 | public function setLabel($key, $value) 127 | { 128 | $this->labels()->put($key, $value); 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Setter for post type arguments. 135 | * 136 | * @param string $key 137 | * @param mixed $value 138 | * 139 | * @return $this 140 | */ 141 | public function set($key, $value) 142 | { 143 | $this->args->put($key, $value); 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * Getter for post type arguments. 150 | * 151 | * @param string $key 152 | * 153 | * @return mixed 154 | */ 155 | public function get($key) 156 | { 157 | return $this->args->get($key); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Type/Labels.php: -------------------------------------------------------------------------------- 1 | singularForm = $label; 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * Set the plural labels using the given form. 37 | * 38 | * @param string $label The plural label form to use 39 | * 40 | * @return $this 41 | */ 42 | public function setPlural($label) 43 | { 44 | $this->pluralForm = $label; 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * Get all the labels as an array. 51 | * 52 | * @return array 53 | */ 54 | public function toArray() 55 | { 56 | return $this->map(function ($label) { 57 | return str_replace( 58 | [ 59 | '{one}', 60 | '{many}' 61 | ], 62 | [ 63 | $this->singularForm, 64 | $this->pluralForm 65 | ], 66 | $label 67 | ); 68 | })->all(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Type/Model.php: -------------------------------------------------------------------------------- 1 | newInstanceArgs($arguments); 72 | } 73 | 74 | return new static; 75 | } 76 | 77 | /** 78 | * Fill the model with an array of attributes. 79 | * 80 | * @param array $attributes 81 | * 82 | * @return $this 83 | */ 84 | public function fill(array $attributes) 85 | { 86 | foreach ($attributes as $key => $value) { 87 | if ($this->expandAlias($key)) { 88 | $this->aliasSet($key, $value); 89 | continue; 90 | } 91 | 92 | $this->object->$key = $value; 93 | } 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * Create a new model of the model's type, and save it to the database. 100 | * 101 | * @param array $attributes 102 | * 103 | * @return static 104 | */ 105 | public static function create($attributes = []) 106 | { 107 | $model = new static($attributes); 108 | 109 | return $model->save(); 110 | } 111 | 112 | /** 113 | * Create a new query builder instance for this model type. 114 | * 115 | * @return \Silk\Query\Builder 116 | */ 117 | public static function query() 118 | { 119 | return (new static)->newQuery(); 120 | } 121 | 122 | /** 123 | * Meta API for this type 124 | * 125 | * @param string $key Meta key to retrieve or empty to retrieve all. 126 | * 127 | * @return ObjectMeta|\Silk\Meta\Meta 128 | */ 129 | public function meta($key = '') 130 | { 131 | $meta = new ObjectMeta(static::OBJECT_TYPE, $this->id); 132 | 133 | if ($key) { 134 | return $meta->get($key); 135 | } 136 | 137 | return $meta; 138 | } 139 | 140 | /** 141 | * Set the primary ID on the model. 142 | * 143 | * @param string|int $id The model's ID 144 | * 145 | * @return $this 146 | */ 147 | protected function setId($id) 148 | { 149 | $this->object->{static::ID_PROPERTY} = (int) $id; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * Set the object for the model. 156 | * 157 | * @param $object 158 | * 159 | * @return $this 160 | */ 161 | protected function setObject($object) 162 | { 163 | $this->object = $object; 164 | 165 | return $this; 166 | } 167 | 168 | /** 169 | * @return array 170 | */ 171 | protected function objectAliases() 172 | { 173 | return []; 174 | } 175 | 176 | /** 177 | * @return object 178 | */ 179 | protected function getAliasedObject() 180 | { 181 | return $this->object; 182 | } 183 | 184 | /** 185 | * Magic getter. 186 | * 187 | * @param string $property 188 | * 189 | * @return mixed 190 | */ 191 | public function __get($property) 192 | { 193 | if ($property == 'id') { 194 | return $this->object->{static::ID_PROPERTY}; 195 | } 196 | 197 | if (in_array($property, ['object', static::OBJECT_TYPE])) { 198 | return $this->object; 199 | } 200 | 201 | if (! is_null($aliased = $this->aliasGet($property))) { 202 | return $aliased; 203 | } 204 | 205 | /** 206 | * Finally, hand-off the request to the wrapped object. 207 | * We don't check for existence as we leverage the magic __get 208 | * on the wrapped object as well. 209 | */ 210 | return $this->object->$property; 211 | } 212 | 213 | /** 214 | * Magic Isset Checker. 215 | * 216 | * @param $property 217 | * 218 | * @return bool 219 | */ 220 | public function __isset($property) 221 | { 222 | return ! is_null($this->__get($property)); 223 | } 224 | 225 | /** 226 | * Magic setter. 227 | * 228 | * @param string $property The property name 229 | * @param mixed $value The new property value 230 | */ 231 | public function __set($property, $value) 232 | { 233 | if ($this->aliasSet($property, $value)) { 234 | return; 235 | } 236 | 237 | $this->object->$property = $value; 238 | } 239 | 240 | /** 241 | * Handle dynamic method calls into the model. 242 | * 243 | * @param string $method 244 | * @param array $arguments 245 | * 246 | * @return mixed 247 | */ 248 | public function __call($method, $arguments) 249 | { 250 | $query = $this->newQuery(); 251 | 252 | return call_user_func_array([$query, $method], $arguments); 253 | } 254 | 255 | /** 256 | * Handle dynamic static method calls on the model class. 257 | * 258 | * Proxies calls to direct method calls on a new instance 259 | * 260 | * @param string $method 261 | * @param array $arguments 262 | * 263 | * @return mixed 264 | */ 265 | public static function __callStatic($method, array $arguments) 266 | { 267 | return call_user_func_array([new static, $method], $arguments); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/Type/ObjectAliases.php: -------------------------------------------------------------------------------- 1 | 'propertyNameOnObject'] 11 | * 12 | * @return array 13 | */ 14 | abstract protected function objectAliases(); 15 | 16 | /** 17 | * Get the aliased object instance. 18 | * 19 | * @return object 20 | */ 21 | abstract protected function getAliasedObject(); 22 | 23 | /** 24 | * Get a property from the aliased object by the model's key. 25 | * 26 | * @param $key 27 | * 28 | * @return mixed|null 29 | */ 30 | protected function aliasGet($key) 31 | { 32 | if (! $expanded = $this->expandAlias($key)) { 33 | return null; 34 | } 35 | 36 | return data_get($this->getAliasedObject(), $expanded); 37 | } 38 | 39 | /** 40 | * Set a property on the aliased object. 41 | * 42 | * @param string $key The alias name on the model 43 | * @param mixed $value The value to set on the aliased object 44 | * 45 | * @return bool True if the alias was resolved and set; otherwise false 46 | */ 47 | protected function aliasSet($key, $value) 48 | { 49 | $expanded = $this->expandAlias($key); 50 | 51 | if ($expanded && is_object($aliased = $this->getAliasedObject())) { 52 | $aliased->$expanded = $value; 53 | return true; 54 | } 55 | 56 | return false; 57 | } 58 | 59 | /** 60 | * Expands an alias into its respective object property name. 61 | * 62 | * @param string $key Alias key 63 | * 64 | * @return string|false The expanded alias, or false no alias exists for the key. 65 | */ 66 | protected function expandAlias($key) 67 | { 68 | return data_get($this->objectAliases(), $key, false); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Type/ShorthandProperties.php: -------------------------------------------------------------------------------- 1 | getAliasedObject(); 19 | 20 | if (is_object($aliased) || ! empty(static::OBJECT_TYPE)) { 21 | /** 22 | * Automatically alias shorthand syntax for type_name 23 | * Eg: 'post_content' is aliased to 'content' 24 | */ 25 | $expanded = static::OBJECT_TYPE . '_' . $key; 26 | 27 | if (property_exists($aliased, $expanded)) { 28 | return $expanded; 29 | } 30 | } 31 | 32 | return parent::expandAlias($key); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Type/Type.php: -------------------------------------------------------------------------------- 1 | 'name', 28 | 'slug' => 'name', 29 | 'one' => 'labels.singular_name', 30 | 'many' => 'labels.name', 31 | ]; 32 | } 33 | 34 | /** 35 | * @return object 36 | */ 37 | protected function getAliasedObject() 38 | { 39 | return $this->object; 40 | } 41 | 42 | /** 43 | * Magic Getter. 44 | * 45 | * @param string $property Accessed property name 46 | * 47 | * @return mixed 48 | */ 49 | public function __get($property) 50 | { 51 | if (! is_null($aliased = $this->aliasGet($property))) { 52 | return $aliased; 53 | } 54 | 55 | return data_get($this->object, $property); 56 | } 57 | 58 | /** 59 | * Magic Isset Check. 60 | * 61 | * @param string $property Queried property name 62 | * 63 | * @return boolean 64 | */ 65 | public function __isset($property) 66 | { 67 | return ! is_null($this->__get($property)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/User/Exception/UserNotFoundException.php: -------------------------------------------------------------------------------- 1 | object = $this->normalizeData($user); 37 | 38 | $this->fill($attributes); 39 | } 40 | 41 | /** 42 | * Retrieve a new instance by the ID. 43 | * 44 | * @param int|string $id Primary ID 45 | * 46 | * @return null|static 47 | */ 48 | public static function find($id) 49 | { 50 | try { 51 | return static::fromID($id); 52 | } catch (\Exception $e) { 53 | return null; 54 | } 55 | } 56 | 57 | /** 58 | * Create a new instance from the user ID. 59 | * 60 | * @param string|int $id User ID 61 | * 62 | * @throws UserNotFoundException 63 | * 64 | * @return static 65 | */ 66 | public static function fromID($id) 67 | { 68 | if (! $user = get_user_by('id', $id)) { 69 | throw new UserNotFoundException("No user found with ID $id"); 70 | } 71 | 72 | return new static($user); 73 | } 74 | 75 | /** 76 | * Create a new instance from the username. 77 | * 78 | * @param string $username Username (login) 79 | * 80 | * @throws UserNotFoundException 81 | * 82 | * @return static 83 | */ 84 | public static function fromUsername($username) 85 | { 86 | if (! $user = get_user_by('login', $username)) { 87 | throw new UserNotFoundException("No user found with username: $username"); 88 | } 89 | 90 | return new static($user); 91 | } 92 | 93 | /** 94 | * Create a new instance from the user's email address. 95 | * 96 | * @param string $email User email address 97 | * 98 | * @throws UserNotFoundException 99 | * 100 | * @return static 101 | */ 102 | public static function fromEmail($email) 103 | { 104 | if (! $user = get_user_by('email', $email)) { 105 | throw new UserNotFoundException("No user found with email address: $email"); 106 | } 107 | 108 | return new static($user); 109 | } 110 | 111 | /** 112 | * Create a new instance from the user's slug. 113 | * 114 | * @param string $slug User slug (nicename) 115 | * 116 | * @throws UserNotFoundException 117 | * 118 | * @return static 119 | */ 120 | public static function fromSlug($slug) 121 | { 122 | if (! $user = get_user_by('slug', $slug)) { 123 | throw new UserNotFoundException("No user found with slug: $slug"); 124 | } 125 | 126 | return new static($user); 127 | } 128 | 129 | /** 130 | * Create a new instance using the currently authenticated user. 131 | * 132 | * @return static 133 | */ 134 | public static function auth() 135 | { 136 | return new static(wp_get_current_user()); 137 | } 138 | 139 | /** 140 | * Get the URL for the user's posts archive. 141 | * 142 | * @return string 143 | */ 144 | public function postsUrl() 145 | { 146 | return get_author_posts_url($this->id, $this->slug); 147 | } 148 | 149 | /** 150 | * Get a new query builder for the model. 151 | * 152 | * @return QueryBuilder 153 | */ 154 | public function newQuery() 155 | { 156 | return QueryBuilder::make()->setModel($this); 157 | } 158 | 159 | /** 160 | * Save the changes to the database. 161 | * 162 | * @throws WP_ErrorException 163 | * 164 | * @return $this 165 | */ 166 | public function save() 167 | { 168 | if (! $this->id) { 169 | $result = wp_insert_user($this->object); 170 | } else { 171 | $result = wp_update_user($this->object); 172 | } 173 | 174 | if (is_wp_error($result)) { 175 | throw new WP_ErrorException($result); 176 | } 177 | 178 | $this->setId($result)->refresh(); 179 | 180 | return $this; 181 | } 182 | 183 | /** 184 | * Delete the modeled record from the database. 185 | * 186 | * @return $this 187 | */ 188 | public function delete() 189 | { 190 | if (wp_delete_user($this->id)) { 191 | $this->setObject(new WP_User); 192 | } 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * Reload the object from the database. 199 | * 200 | * @return $this 201 | */ 202 | public function refresh() 203 | { 204 | $this->setObject(new WP_User($this->id)); 205 | 206 | return $this; 207 | } 208 | 209 | /** 210 | * Set the WP_User object on the model. 211 | * 212 | * @param WP_User $user 213 | * 214 | * @return $this 215 | */ 216 | protected function setObject($user) 217 | { 218 | $this->object = $this->normalizeData($user); 219 | 220 | return $this; 221 | } 222 | 223 | /** 224 | * Normalize the user data object on the given User. 225 | * 226 | * This is necessary for object aliases and shorthand properties to work properly 227 | * due to the fact that the WP_User's data object is a plain object which 228 | * does not always contain all properties as is the case with other WP objects. 229 | * 230 | * @param WP_User $user 231 | * 232 | * @return WP_User 233 | */ 234 | protected function normalizeData(WP_User $user) 235 | { 236 | Collection::make([ 237 | 'ID', 238 | 'user_login', 239 | 'user_pass', 240 | 'user_nicename', 241 | 'user_email', 242 | 'user_registered', 243 | 'user_activation_key', 244 | 'user_status', 245 | 'display_name', 246 | 'spam', 247 | 'deleted', 248 | ])->diff(array_keys((array) $user->data)) 249 | ->each(function ($property) use ($user) { 250 | $user->data->$property = null; // exists but ! isset 251 | }); 252 | 253 | return $user; 254 | } 255 | 256 | /** 257 | * Get the aliased object. 258 | * 259 | * Most user data from the database is stored as an object on the user's `data` property. 260 | * 261 | * @return object|\stdClass 262 | */ 263 | protected function getAliasedObject() 264 | { 265 | return $this->object->data; 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/User/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | query = $query; 31 | } 32 | 33 | /** 34 | * Create a new instance. 35 | * 36 | * @param WP_User_Query $query 37 | * 38 | * @return static 39 | */ 40 | public static function make(WP_User_Query $query = null) 41 | { 42 | return new static($query); 43 | } 44 | 45 | /** 46 | * Execute the query and return the raw results. 47 | * 48 | * @return array 49 | */ 50 | protected function query() 51 | { 52 | $this->set('fields', 'all'); 53 | 54 | $this->query->prepare_query(); 55 | $this->query->query(); 56 | 57 | return $this->query->get_results(); 58 | } 59 | 60 | /** 61 | * Set an arbitrary query parameter. 62 | * 63 | * @param $parameter 64 | * @param $value 65 | * 66 | * @return $this 67 | */ 68 | public function set($parameter, $value) 69 | { 70 | $this->query->set($parameter, $value); 71 | 72 | return $this; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/WordPress/Post/Attachment.php: -------------------------------------------------------------------------------- 1 | setCallback($callback) 19 | ->listen(); 20 | } 21 | endif; 22 | 23 | if (! function_exists('off')) : 24 | /** 25 | * Remove an event listener. 26 | * 27 | * If the callback cannot be removed immediately, attempt to remove it just-in-time as a fallback. 28 | * 29 | * @param string $handle action or filter handle 30 | * @param callable $callback 31 | * @param int $priority 32 | * 33 | * @return bool|Hook true if immediately removed, Hook instance otherwise 34 | */ 35 | function off($handle, $callback, $priority = 10) 36 | { 37 | if ($removed = remove_filter($handle, $callback, $priority)) { 38 | return $removed; 39 | } 40 | 41 | /** 42 | * If the hook was not able to be removed above, then it has not been set yet. 43 | * Here we add a new listener right before the hook is expected to fire, 44 | * so that if it is there, we can unhook it just in time. 45 | */ 46 | return on($handle, function ($given = null) use ($handle, $callback, $priority) { 47 | remove_filter($handle, $callback, $priority); 48 | return $given; 49 | })->withPriority($priority - 1); 50 | } 51 | endif; 52 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | createManyTags($count); 8 | $this->factory()->term->add_post_terms($post_id, $tags, 'post_tag'); 9 | } 10 | 11 | protected function createManyCatsForPost($count, $post_id) 12 | { 13 | $tags = $this->createManyCats($count); 14 | $this->factory()->term->add_post_terms($post_id, $tags, 'category'); 15 | } 16 | 17 | protected function createManyTags($count) 18 | { 19 | return $this->factory()->term->create_many($count, ['taxonomy' => 'post_tag']); 20 | } 21 | 22 | protected function createManyCats($count) 23 | { 24 | return $this->factory()->term->create_many($count, ['taxonomy' => 'category']); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/unit/Event/HookTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Hook::class, $hook); 14 | $this->assertInstanceOf(Hook::class, $easy); 15 | } 16 | 17 | /** @test */ 18 | function it_uses_a_fluent_api() 19 | { 20 | $hook = Hook::on('asdf') 21 | ->setCallback(function () {}) 22 | ->withPriority(99) 23 | ->once() 24 | ->listen(); 25 | 26 | $this->assertInstanceOf(Hook::class, $hook); 27 | } 28 | 29 | /** @test */ 30 | function it_calls_the_callback_we_give_it() 31 | { 32 | $data = ''; 33 | 34 | $hook = Hook::on('some_action') 35 | ->setCallback(function ($given) use (&$data) { 36 | $data = $given; 37 | }) 38 | ->listen(); 39 | 40 | do_action('some_action', 'Howdy!'); 41 | 42 | $this->assertEquals($data, 'Howdy!'); 43 | 44 | apply_filters('some_action', 'Filter this!'); 45 | 46 | $this->assertEquals($data, 'Filter this!'); 47 | 48 | $data = ''; 49 | $lazy = on('quick', function ($given) use (&$data) { 50 | $data = $given; 51 | }); 52 | 53 | do_action('quick', 'yo'); 54 | 55 | $this->assertEquals($data, 'yo'); 56 | } 57 | 58 | /** @test */ 59 | function it_listens_on_the_priority_we_set() 60 | { 61 | $data = 'this is passed by reference to the callback'; 62 | 63 | $iterate = function ($value) { 64 | return ++$value; 65 | }; 66 | add_filter('filterme', $iterate, 1); 67 | add_filter('filterme', $iterate, 2); 68 | add_filter('filterme', $iterate, 3); 69 | add_filter('filterme', $iterate, 4); 70 | // check here 71 | add_filter('filterme', $iterate, 6); 72 | add_filter('filterme', $iterate, 7); 73 | add_filter('filterme', $iterate, 8); 74 | 75 | Hook::on('filterme') 76 | ->setCallback(function ($value) use (&$data) { 77 | $data = $value; 78 | }) 79 | ->withPriority(5) 80 | ->listen(); 81 | 82 | apply_filters('filterme', 1); 83 | 84 | $this->assertEquals(5, $data); 85 | } 86 | 87 | /** @test */ 88 | function it_passes_the_correct_number_of_arguments_to_the_callback_automatically() 89 | { 90 | $arguments_count = null; 91 | 92 | $hook = Hook::on('testing_arguments_passed') 93 | ->setCallback(function ($one, $two, $three) use (&$arguments_count) { 94 | $arguments_count = func_num_args(); 95 | }) 96 | ->listen(); 97 | 98 | do_action('testing_arguments_passed', 1, 2, 3); 99 | 100 | $this->assertEquals(3, $arguments_count); 101 | 102 | $arguments_count = null; 103 | $hook->setCallback(function ($one, $two) use (&$arguments_count) { 104 | $arguments_count = func_num_args(); 105 | }); 106 | 107 | do_action('testing_arguments_passed', 1, 2, 3); 108 | 109 | $this->assertEquals(2, $arguments_count); 110 | } 111 | 112 | /** @test */ 113 | function it_passes_all_arguments_to_a_callback_that_has_no_parameters() 114 | { 115 | $passed = 0; 116 | Hook::on('test_all_arguments_passed') 117 | ->setCallback(function () use (&$passed) { 118 | $passed = func_num_args(); 119 | })->listen(); 120 | 121 | do_action('test_all_arguments_passed', 1, 2, 3); 122 | 123 | $this->assertEquals(3, $passed); 124 | } 125 | 126 | 127 | /** @test */ 128 | function it_can_limit_the_number_of_times_the_callback_is_invoked() 129 | { 130 | $count = 0; 131 | 132 | Hook::on('three_times_only_test') 133 | ->setCallback(function () use (&$count) { 134 | $count++; 135 | }) 136 | ->listen() 137 | ->onlyXtimes(3); 138 | 139 | do_action('three_times_only_test'); 140 | do_action('three_times_only_test'); 141 | do_action('three_times_only_test'); 142 | do_action('three_times_only_test'); 143 | do_action('three_times_only_test'); 144 | do_action('three_times_only_test'); 145 | 146 | $this->assertEquals(3, $count); 147 | } 148 | 149 | /** @test */ 150 | function it_has_a_helper_method_for_bypassing_the_callback() 151 | { 152 | $count = 0; 153 | 154 | $hook = Hook::on('bypass_test') 155 | ->setCallback(function () use (&$count) { 156 | $count++; 157 | }) 158 | ->listen(); 159 | 160 | do_action('bypass_test'); 161 | do_action('bypass_test'); 162 | do_action('bypass_test'); 163 | do_action('bypass_test'); 164 | do_action('bypass_test'); // 5 165 | 166 | $hook->bypass(); // callback will not be triggered again 167 | 168 | do_action('bypass_test'); 169 | do_action('bypass_test'); 170 | do_action('bypass_test'); 171 | 172 | $this->assertEquals(5, $count); 173 | } 174 | 175 | 176 | /** @test */ 177 | function it_can_be_set_to_only_fire_once() 178 | { 179 | $count = 0; 180 | 181 | Hook::on('only_once_test') 182 | ->setCallback(function () use (&$count) { 183 | $count++; 184 | }) 185 | ->once() 186 | ->listen(); 187 | 188 | do_action('only_once_test'); 189 | do_action('only_once_test'); 190 | do_action('only_once_test'); 191 | 192 | $this->assertEquals(1, $count); 193 | 194 | 195 | Hook::on('only_once_filtered') 196 | ->setCallback(function ($value) { 197 | $value *= 2; 198 | return $value; 199 | }) 200 | ->once() 201 | ->listen(); 202 | 203 | $result = apply_filters('only_once_filtered', 1); 204 | $result = apply_filters('only_once_filtered', $result); 205 | $result = apply_filters('only_once_filtered', $result); 206 | 207 | $this->assertEquals(2, $result); 208 | } 209 | 210 | /** @test */ 211 | function it_can_remove_its_hook_if_needed() 212 | { 213 | $hook = Hook::on('remove_this_test') 214 | ->setCallback(function () { 215 | throw new Exception('Test failed!'); 216 | }) 217 | ->listen(); 218 | 219 | $this->assertTrue(has_action('remove_this_test')); 220 | 221 | $hook->remove(); 222 | 223 | $this->assertFalse(has_action('remove_this_test')); 224 | 225 | do_action('remove_this_test'); 226 | } 227 | 228 | /** @test */ 229 | function it_has_a_helper_function_for_removing_hooks_now_and_in_the_future() 230 | { 231 | // exhibit A 232 | add_action('hook_one', 'cb_one'); 233 | $this->assertTrue(off('hook_one', 'cb_one')); 234 | 235 | // exhibit B 236 | $boom = function () { 237 | throw Exception('This should be removed or the test will fail!'); 238 | }; 239 | $hook = off('hook_two', $boom); 240 | $this->assertInstanceOf(Hook::class, $hook); 241 | // could be added waaaay later, sometime, we don't even know 242 | add_action('hook_two', $boom); 243 | 244 | do_action('hook_two'); 245 | } 246 | 247 | /** @test */ 248 | function it_handles_different_callable_syntaxes() 249 | { 250 | Hook::on('test_function_name_as_string') 251 | ->setCallback('aNormalFunction') 252 | ->listen(); 253 | 254 | do_action('test_function_name_as_string'); 255 | 256 | Hook::on('test_static_method_as_string') 257 | ->setCallback('CallMy::staticMethod') 258 | ->listen(); 259 | 260 | do_action('test_static_method_as_string'); 261 | 262 | Hook::on('test_static_method_as_array') 263 | ->setCallback(['CallMy', 'staticMethod']) 264 | ->listen(); 265 | 266 | do_action('test_static_method_as_array'); 267 | 268 | Hook::on('test_instance_method_as_array') 269 | ->setCallback([new CallMy, 'instanceMethod']) 270 | ->listen(); 271 | 272 | do_action('test_instance_method_as_array'); 273 | } 274 | 275 | /** @test */ 276 | function it_can_accept_a_condition_to_control_the_invocation_of_the_callback() 277 | { 278 | on('conditional_test', static function () { 279 | throw new Exception("Don't let this happen!"); 280 | })->onlyIf(static function ($should_blow_up) { 281 | return $should_blow_up; 282 | }); 283 | 284 | do_action('conditional_test', false); 285 | 286 | $sum = 0; 287 | 288 | on('conditional_addition', function ($num) use (&$sum) { 289 | $sum += $num; 290 | })->onlyIf(function ($num) { 291 | return 0 === $num % 2; // return true for even numbers 292 | }); 293 | 294 | do_action('conditional_addition', 1); // ignored 295 | do_action('conditional_addition', 2); // HIT 296 | do_action('conditional_addition', 3); // ignored 297 | do_action('conditional_addition', 4); // HIT 298 | do_action('conditional_addition', 5); // ignored 299 | do_action('conditional_addition', 6); // HIT 300 | do_action('conditional_addition', 7); // ignored 301 | 302 | $this->assertEquals(2 + 4 + 6, $sum); 303 | 304 | // complex using filter 305 | 306 | on('complex_condition', function ($names, $name) { 307 | $names[] = $name; 308 | return $names; 309 | })->onlyIf(function ($names, $name) { 310 | return is_string($name) && strlen($name) > 3; 311 | })->onlyIf(function ($names, $name, $status) { 312 | return in_array($status, ['naughty', 'nice','salamander']); 313 | })->onlyIf(function ($names, $name) { 314 | return ! in_array($name, $names); 315 | }); 316 | 317 | $names = []; 318 | $names = apply_filters('complex_condition', $names, 'Donald', 'naughty'); 319 | $names = apply_filters('complex_condition', $names, 'Hillary', 'naughty'); 320 | $names = apply_filters('complex_condition', $names, 'Barack', 'in-the-house'); // x 321 | $names = apply_filters('complex_condition', $names, 'Ted', 'nice'); // x 322 | $names = apply_filters('complex_condition', $names, 'Bill', 'nice'); 323 | $names = apply_filters('complex_condition', $names, 'Evil Bill', 'naughty'); 324 | $names = apply_filters('complex_condition', $names, 'Donald', 'salamander'); 325 | $names = apply_filters('complex_condition', $names, 'Donald', 'salamander'); 326 | $names = apply_filters('complex_condition', $names, 'Donald', 'salamander'); 327 | 328 | $this->assertSame(['Donald', 'Hillary', 'Bill', 'Evil Bill'], $names); 329 | } 330 | 331 | /** @test */ 332 | function it_returns_the_first_parameter_if_the_callback_returns_nothing() 333 | { 334 | $spy = 'spy'; 335 | 336 | on('filter_as_action_test', function () use (&$spy) { 337 | $spy = 'spider'; 338 | }); 339 | 340 | $filtered = apply_filters('filter_as_action_test', 'something'); 341 | 342 | $this->assertSame('spider', $spy); // ensures callback was called 343 | /** 344 | * Our callback returned nothing, therefore Hook will return for us. 345 | */ 346 | $this->assertSame('something', $filtered); 347 | } 348 | 349 | } 350 | 351 | function aNormalFunction() 352 | { 353 | } 354 | 355 | class CallMy 356 | { 357 | public static function staticMethod() 358 | { 359 | } 360 | 361 | public function instanceMethod() 362 | { 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /tests/unit/Meta/MetaTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('the value', $meta->get()); 15 | $this->assertEquals('the value', (string) $meta); 16 | } 17 | 18 | /** @test */ 19 | function it_sets_the_single_value_for_a_post_meta_key() 20 | { 21 | $meta = new Meta('post', 123, 'some_meta_key'); 22 | 23 | $meta->set('new value'); 24 | 25 | $wp_value = get_post_meta(123, 'some_meta_key', true); 26 | 27 | $this->assertEquals('new value', $wp_value); 28 | } 29 | 30 | /** @test */ 31 | function it_can_update_a_single_value() 32 | { 33 | $meta = new Meta('post', 123, 'many'); 34 | 35 | add_post_meta(123, 'many', 'one'); 36 | add_post_meta(123, 'many', 'two'); 37 | add_post_meta(123, 'many', 'three'); 38 | 39 | $meta->replace('two', 'zwei') 40 | ->replace('three', 'drei'); 41 | 42 | $this->assertSame(['one','zwei','drei'], $meta->all()); 43 | } 44 | 45 | /** @test */ 46 | function it_can_check_for_the_existence_of_any_value() 47 | { 48 | $meta = new Meta('post', 123, 'some_nonexistent_meta_key'); 49 | 50 | $this->assertFalse($meta->exists()); 51 | 52 | $meta->set("I'm ALIVEEEE"); 53 | 54 | $this->assertTrue($meta->exists()); 55 | } 56 | 57 | /** @test */ 58 | function it_can_add_meta_for_keys_with_multiple_values() 59 | { 60 | $meta = new Meta('post', 123, 'many_values'); 61 | $this->assertCount(0, $meta->all()); 62 | 63 | $meta->add('one'); 64 | $meta->add('two'); 65 | $meta->add('three'); 66 | 67 | $this->assertCount(3, $meta->all()); 68 | } 69 | 70 | /** @test */ 71 | function it_can_delete_meta_for_a_key() 72 | { 73 | $meta = new Meta('post', 123, 'temp'); 74 | $meta->set('this value is about to be deleted'); 75 | 76 | $this->assertTrue($meta->exists()); 77 | 78 | $meta->delete(); 79 | 80 | $this->assertFalse($meta->exists()); 81 | 82 | // Multiple values 83 | $meta->add('one') 84 | ->add('two') 85 | ->add('three'); 86 | 87 | // delete a specific value 88 | $meta->delete('one') 89 | ->delete('three'); 90 | 91 | $this->assertSame(['two'], $meta->all()); 92 | } 93 | 94 | /** @test */ 95 | function it_can_return_all_meta_as_an_array_or_a_collection() 96 | { 97 | $meta = new Meta('post', 123, 'many_values'); 98 | $meta->add('one') 99 | ->add('two') 100 | ->add('three'); 101 | 102 | $this->assertSame(['one','two','three'], $meta->all()); 103 | $this->assertInstanceOf(Collection::class, $meta->collect()); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/unit/Meta/ObjectMetaTest.php: -------------------------------------------------------------------------------- 1 | factory()->post->create(); 13 | 14 | $postMeta = new ObjectMeta('post', $post_id); 15 | 16 | $this->assertInstanceOf(Meta::class, $postMeta->get('some_meta_key')); 17 | } 18 | 19 | /** @test */ 20 | function it_can_return_all_meta_as_a_collection() 21 | { 22 | $post_id = $this->factory()->post->create(); 23 | 24 | $meta = new ObjectMeta('post', $post_id); 25 | 26 | $this->assertInstanceOf(Collection::class, $meta->collect()); 27 | 28 | foreach ($meta->collect() as $metaForKey) { 29 | $this->assertInstanceOf(Meta::class, $metaForKey); 30 | } 31 | } 32 | 33 | /** @test */ 34 | function it_can_return_all_meta_as_an_array() 35 | { 36 | /** 37 | * Use a made up post ID so that we can be sure these are the only meta values. 38 | * @var integer 39 | */ 40 | $post_id = 100; 41 | $meta = new ObjectMeta('post', $post_id); 42 | 43 | update_post_meta($post_id, 'a', '1', true); 44 | update_post_meta($post_id, 'b', '2', true); 45 | 46 | $this->assertSame([ 47 | 'a' => ['1'], 48 | 'b' => ['2'] 49 | ], 50 | $meta->toArray() 51 | ); 52 | } 53 | 54 | /** @test */ 55 | function it_has_readonly_properties() 56 | { 57 | $meta = new ObjectMeta('post', 123); 58 | 59 | $this->assertSame('post', $meta->type); 60 | $this->assertSame(123, $meta->id); 61 | 62 | $this->assertNull($meta->non_existent); 63 | } 64 | 65 | /** @test */ 66 | function it_has_a_fluent_setter() 67 | { 68 | $meta = new ObjectMeta('post', 123); 69 | 70 | $meta->set('a', 'b') 71 | ->set('foo', 'bar'); 72 | 73 | $this->assertSame([ 74 | 'a' => ['b'], 75 | 'foo' => ['bar'] 76 | ], 77 | get_metadata('post', 123) 78 | ); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /tests/unit/Post/PageTest.php: -------------------------------------------------------------------------------- 1 | factory()->post->create_and_get(['post_type' => 'page']); 13 | 14 | $model_from_id = Page::fromID($page->ID); 15 | $model_from_obj = Page::make($page); 16 | $model_from_slug = Page::fromSlug($page->post_name); 17 | 18 | $this->assertSame($page->ID, $model_from_id->id); 19 | $this->assertSame($page->ID, $model_from_obj->id); 20 | $this->assertSame($page->ID, $model_from_slug->id); 21 | } 22 | 23 | /** @test */ 24 | function it_can_create_a_page_from_a_new_instance() 25 | { 26 | $model = new Page; 27 | $model->post_title = 'some title'; 28 | $model->save(); 29 | 30 | $this->assertGreaterThan(0, $model->id); 31 | 32 | $this->assertSame('page', $model->post_type); 33 | } 34 | 35 | /** 36 | * @test 37 | * @expectedException \Silk\Post\Exception\ModelPostTypeMismatchException 38 | */ 39 | function it_blows_up_if_instantiated_with_a_non_page_post_type() 40 | { 41 | $post_id = $this->factory()->post->create(['post_type' => 'post']); 42 | 43 | // this will blow up since the post id is for a post_type of `post` 44 | Page::fromID($post_id); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /tests/unit/Post/PostModelTest.php: -------------------------------------------------------------------------------- 1 | assertSame('event', ModelTestEvent::postTypeId()); 12 | $this->assertSame('event', ModelTestEventTrait::postTypeId()); 13 | $this->assertSame('model_test_post_type', ModelTestPostType::postTypeId()); 14 | $this->assertSame('dinosaur', Dinosaur::postTypeId()); 15 | } 16 | 17 | /** @test */ 18 | function it_has_a_method_for_getting_the_post_type_api() 19 | { 20 | $this->assertInstanceOf(Builder::class, Dinosaur::postType()); 21 | } 22 | 23 | /** @test */ 24 | function it_has_a_named_constructor_to_make_a_new_instance() 25 | { 26 | $this->assertInstanceOf(Dinosaur::class, Dinosaur::make()); 27 | } 28 | 29 | /** @test */ 30 | function the_make_method_passes_its_arguments_to_the_constructor() 31 | { 32 | $wp_post = $this->factory()->post->create_and_get(['post_type' => 'event']); 33 | $model = ModelTestEvent::make($wp_post); 34 | 35 | $this->assertSame($wp_post, $model->object); 36 | } 37 | 38 | /** @test */ 39 | function it_can_create_a_new_post_with_shorthand_attributes() 40 | { 41 | $model = ModelTestShorthand::create([ 42 | 'title' => 'The Title', 43 | 'name' => 'urlish-title', 44 | 'excerpt' => 'something cool', 45 | 'post_content' => 'Some content', // required to test parent method 46 | ]); 47 | 48 | $post = get_post($model->id); 49 | 50 | $this->assertSame('The Title', $post->post_title); 51 | $this->assertSame('urlish-title', $post->post_name); 52 | $this->assertSame('something cool', $post->post_excerpt); 53 | $this->assertSame('Some content', $post->post_content); 54 | } 55 | 56 | /** @test */ 57 | function it_has_models_for_all_builtin_post_types() 58 | { 59 | $this->assertSame('attachment' , \Silk\WordPress\Post\Attachment::postTypeId()); 60 | $this->assertSame('nav_menu_item' , \Silk\WordPress\Post\NavMenuItem::postTypeId()); 61 | $this->assertSame('page' , \Silk\WordPress\Post\Page::postTypeId()); 62 | $this->assertSame('post' , \Silk\WordPress\Post\Post::postTypeId()); 63 | $this->assertSame('revision' , \Silk\WordPress\Post\Revision::postTypeId()); 64 | } 65 | } 66 | 67 | /** 68 | * Models post with post_type 'event' 69 | */ 70 | class ModelTestEvent extends Model 71 | { 72 | const POST_TYPE = 'event'; 73 | } 74 | 75 | /** 76 | * Models post with post_type 'dinosaur' 77 | */ 78 | class Dinosaur extends Model 79 | { 80 | use Silk\Post\ClassNameAsPostType; 81 | } 82 | 83 | /** 84 | * Models post with post_type 'model_test_post_type' 85 | */ 86 | class ModelTestPostType extends Model 87 | { 88 | use Silk\Post\ClassNameAsPostType; 89 | } 90 | 91 | /** 92 | * Models post with post_type 'model_test_post_type' 93 | */ 94 | class ModelTestEventTrait extends ModelTestEvent 95 | { 96 | /** 97 | * Here the trait is overriden by the constant 98 | */ 99 | use Silk\Post\ClassNameAsPostType; 100 | } 101 | 102 | class ModelTestShorthand extends Model 103 | { 104 | use \Silk\Type\ShorthandProperties; 105 | } 106 | -------------------------------------------------------------------------------- /tests/unit/Post/PostQueryBuilderTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(QueryBuilder::class, new QueryBuilder(new WP_Query)); 14 | } 15 | 16 | /** @test */ 17 | function it_returns_the_results_as_a_collection() 18 | { 19 | $builder = new QueryBuilder(new WP_Query); 20 | 21 | $this->assertInstanceOf(Collection::class, $builder->results()); 22 | } 23 | 24 | /** @test */ 25 | function the_results_can_be_limited_to_the_integer_provided() 26 | { 27 | $this->factory()->post->create_many(10); 28 | 29 | $builder = new QueryBuilder(new WP_Query); 30 | $builder->limit(5); 31 | 32 | $this->assertCount(5, $builder->results()); 33 | } 34 | 35 | /** @test */ 36 | function it_has_getters_and_setters_for_holding_the_model_instance() 37 | { 38 | $model = new CustomCPT; 39 | $builder = new QueryBuilder(new WP_Query); 40 | 41 | $builder->setModel($model); 42 | 43 | $this->assertSame($model, $builder->getModel()); 44 | } 45 | 46 | /** @test */ 47 | function it_returns_results_as_a_collection_of_models() 48 | { 49 | register_post_type(CustomCPT::POST_TYPE); 50 | CustomCPT::create(['post_title' => 'check one']); 51 | 52 | $results = CustomCPT::query()->results(); 53 | 54 | $this->assertInstanceOf(Collection::class, $results); 55 | 56 | $this->assertInstanceOf(CustomCPT::class, $results[0]); 57 | } 58 | 59 | /** @test */ 60 | function it_has_methods_for_setting_the_order_of_results() 61 | { 62 | $first_id = $this->factory()->post->create(); 63 | $this->factory()->post->create_many(5); 64 | $last_id = $this->factory()->post->create(); 65 | 66 | $builder = new QueryBuilder(new WP_Query); 67 | $builder->setModel(new Post); 68 | 69 | $builder->order('asc'); 70 | $resultsAsc = $builder->results(); 71 | $this->assertSame($first_id, $resultsAsc->first()->id); 72 | $this->assertSame($last_id, $resultsAsc->last()->id); 73 | 74 | $builder->order('desc'); 75 | $resultsAsc = $builder->results(); 76 | $this->assertSame($first_id, $resultsAsc->last()->id); 77 | $this->assertSame($last_id, $resultsAsc->first()->id); 78 | } 79 | 80 | /** @test */ 81 | function it_can_query_by_status() 82 | { 83 | $this->factory()->post->create_many(5, ['post_status' => 'doggie']); 84 | $builder = new QueryBuilder(new WP_Query); 85 | 86 | $doggies = $builder->whereStatus('doggie')->results(); 87 | $this->assertCount(5, $doggies); 88 | } 89 | 90 | /** @test */ 91 | function it_can_query_by_slug() 92 | { 93 | $post_id = $this->factory()->post->create(['post_name' => 'sluggy']); 94 | $builder = new QueryBuilder(new WP_Query); 95 | $builder->whereSlug('sluggy'); 96 | 97 | $this->assertSame($post_id, $builder->results()->first()->ID); 98 | } 99 | 100 | /** @test */ 101 | function it_can_set_arbitrary_query_vars() 102 | { 103 | $query = new WP_Query('foo=bar'); 104 | $this->assertSame('bar', $query->get('foo')); 105 | 106 | $builder = new QueryBuilder($query); 107 | $builder->set('foo', 'donut'); 108 | 109 | $this->assertSame('donut', $query->get('foo')); 110 | } 111 | 112 | /** @test */ 113 | function it_delegates_query_scopes_to_the_model() 114 | { 115 | $model = new ModelTestScope(); 116 | $this->factory()->post->create_many(3, [ 117 | 'post_type' => $model->post_type, 118 | 'post_status' => 'publish', 119 | ]); 120 | $this->factory()->post->create_many(4, [ 121 | 'post_type' => $model->post_type, 122 | 'post_status' => 'draft', 123 | ]); 124 | $this->factory()->post->create_many(5, [ 125 | 'post_type' => $model->post_type, 126 | 'post_status' => 'inherit', 127 | ]); 128 | 129 | $builder = new QueryBuilder(new WP_Query); 130 | $builder->setModel($model); 131 | 132 | $this->assertCount(3, $builder->published()->results()); 133 | $this->assertCount(4, $builder->draft()->results()); 134 | $this->assertCount(5, $builder->revision()->results()); 135 | } 136 | 137 | /** @test */ 138 | function undefined_scopes_throw_method_not_found_exception() 139 | { 140 | $model = new ModelTestScope(); 141 | $this->factory()->post->create_many(3, [ 142 | 'post_type' => $model->post_type, 143 | 'post_status' => 'publish', 144 | ]); 145 | 146 | $builder = new QueryBuilder(new WP_Query); 147 | $builder->setModel($model); 148 | 149 | $this->assertFalse(method_exists($model, 'scopeNonExistentScope')); 150 | 151 | try { 152 | $builder->nonExistentScope(); 153 | } catch (\BadMethodCallException $e) { 154 | return; 155 | } 156 | 157 | $this->fail('Expected a BadMethodCallException due to missing query scope'); 158 | } 159 | 160 | /** @test */ 161 | function scopes_can_pass_parameters_to_the_model_methods() 162 | { 163 | $model = new ModelTestScope(); 164 | $parent_id = $this->factory()->post->create([ 165 | 'post_type' => $model->post_type, 166 | ]); 167 | 168 | $children = $this->factory()->post->create_many(3, [ 169 | 'post_type' => $model->post_type, 170 | 'post_parent' => $parent_id, 171 | ]); 172 | 173 | $builder = new QueryBuilder(new WP_Query); 174 | $builder->setModel($model); 175 | 176 | $this->assertCount(3, $builder->childOf($parent_id)->results()); 177 | $this->assertEqualSets($children, $builder->childOf($parent_id)->results()->pluck('id')->all()); 178 | } 179 | 180 | /** @test */ 181 | function it_provides_readonly_access_to_the_wrapped_query_object() 182 | { 183 | $query = new WP_Query; 184 | $builder = new QueryBuilder($query); 185 | 186 | $this->assertSame($query, $builder->getQuery()); 187 | } 188 | 189 | } 190 | 191 | class CustomCPT extends Model 192 | { 193 | const POST_TYPE = 'custom'; 194 | } 195 | 196 | class ModelTestScope extends Model 197 | { 198 | const POST_TYPE = 'custom'; 199 | 200 | public function scopeDraft(QueryBuilder $builder) 201 | { 202 | return $builder->whereStatus('draft'); 203 | } 204 | 205 | public function scopePublished(QueryBuilder $builder) 206 | { 207 | return $builder->whereStatus('publish'); 208 | } 209 | 210 | public function scopeRevision(QueryBuilder $builder) 211 | { 212 | return $builder->whereStatus('inherit'); 213 | } 214 | 215 | public function scopeChildOf(QueryBuilder $builder, $parent) 216 | { 217 | return $builder->set('post_parent', $parent); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /tests/unit/Post/PostTest.php: -------------------------------------------------------------------------------- 1 | assertNull($model->id); 12 | $this->assertInstanceOf(WP_Post::class, $model->post); 13 | 14 | $model->post_title = 'the title'; 15 | $model->save(); 16 | 17 | $this->assertNotEmpty($model->post->ID); 18 | } 19 | 20 | /** @test */ 21 | function it_can_create_a_new_instance_using_a_wp_post_object() 22 | { 23 | $wp_post = $this->factory()->post->create_and_get(); 24 | 25 | $model = new Post($wp_post); 26 | 27 | $this->assertSame($wp_post, $model->object); 28 | } 29 | 30 | /** 31 | * @test 32 | * @expectedException \Silk\Post\Exception\ModelPostTypeMismatchException 33 | */ 34 | function it_blows_up_if_instantiated_with_a_post_of_a_different_post_type() 35 | { 36 | $wp_post = $this->factory()->post->create_and_get(['post_type' => 'page']); 37 | 38 | $this->assertSame('page', $wp_post->post_type); 39 | 40 | new Post($wp_post); 41 | } 42 | 43 | /** @test */ 44 | function it_can_be_instantiated_with_an_array_of_attributes() 45 | { 46 | $model = new Post([ 47 | 'post_title' => 'The Title' 48 | ]); 49 | 50 | $this->assertSame('The Title', $model->post_title); 51 | } 52 | 53 | /** @test */ 54 | function it_can_find_a_post_by_the_id() 55 | { 56 | $post_id = $this->factory()->post->create(); 57 | $model = Post::fromID($post_id); 58 | 59 | $this->assertInstanceOf(Post::class, $model); 60 | $this->assertInstanceOf(WP_Post::class, $model->post); 61 | $this->assertEquals($post_id, $model->id); 62 | } 63 | 64 | /** @test */ 65 | function it_can_find_a_post_by_the_slug() 66 | { 67 | $the_slug = 'foo-bar-slug'; 68 | $post_id = $this->factory()->post->create(['post_name' => $the_slug]); 69 | $model = Post::fromSlug($the_slug); 70 | 71 | $this->assertEquals($post_id, $model->id); 72 | } 73 | 74 | /** 75 | * @test 76 | * @expectedException \Silk\Post\Exception\PostNotFoundException 77 | */ 78 | function it_blows_up_if_no_post_is_found_for_given_slug() 79 | { 80 | Post::fromSlug('no-post-here'); 81 | } 82 | 83 | /** 84 | * @test 85 | * @expectedException \Silk\Post\Exception\PostNotFoundException 86 | */ 87 | function it_blows_up_if_no_post_exists_for_given_id() 88 | { 89 | Post::fromID(123958723409817209872350872395872304); 90 | } 91 | 92 | /** @test */ 93 | function it_can_be_created_from_the_global_post() 94 | { 95 | global $post; 96 | 97 | $post = $this->factory()->post->create_and_get(); 98 | $model = Post::fromGlobal(); 99 | 100 | $this->assertSame($post->ID, $model->id); 101 | } 102 | 103 | /** 104 | * @test 105 | * @expectedException \Silk\Post\Exception\PostNotFoundException 106 | */ 107 | function it_blows_up_if_instantiated_from_an_empty_global_post() 108 | { 109 | Post::fromGlobal(); 110 | } 111 | 112 | /** @test */ 113 | function it_proxies_property_access_to_the_post_if_not_available_on_the_instance() 114 | { 115 | $post = $this->factory()->post->create_and_get(); 116 | $model = new Post($post); 117 | 118 | $this->assertEquals($post->post_date, $model->post_date); 119 | $this->assertEquals($post->post_excerpt, $model->post_excerpt); 120 | 121 | $this->assertEmpty($model->some_property); 122 | $model->meta('some_property')->set('awesome'); 123 | 124 | $this->assertSame('awesome', $post->some_property); 125 | $this->assertSame('awesome', $model->some_property); 126 | } 127 | 128 | /** @test */ 129 | function it_provides_an_object_for_interacting_with_the_post_meta() 130 | { 131 | $post_id = $this->factory()->post->create(); 132 | update_post_meta($post_id, 'new_meta', 'so fresh'); 133 | 134 | $post_meta = get_post_custom($post_id); 135 | $model = Post::fromID($post_id); 136 | 137 | $this->assertEquals($post_meta, $model->meta()->toArray()); 138 | 139 | $this->assertInstanceOf(Silk\Meta\Meta::class, $model->meta('new_meta')); 140 | } 141 | 142 | /** @test */ 143 | function it_can_create_a_new_post() 144 | { 145 | $model = Post::create([ 146 | 'post_title' => 'Foo' 147 | ]); 148 | $this->assertInstanceOf(Post::class, $model); 149 | $this->assertGreaterThan(0, $model->id); 150 | 151 | $post = get_post($model->id); 152 | 153 | $this->assertEquals($post->ID, $model->id); 154 | } 155 | 156 | /** 157 | * @test 158 | * @expectedException Silk\Exception\WP_ErrorException 159 | */ 160 | function it_blows_up_if_required_attributes_are_not_passed_when_created() 161 | { 162 | Post::create(); 163 | } 164 | 165 | /** @test */ 166 | function it_creates_a_post_of_the_models_type() 167 | { 168 | $model = CustomTypeStub::create(['post_title' => 'This is just a test']); 169 | 170 | $this->assertEquals(CustomTypeStub::POST_TYPE, $model->post_type); 171 | } 172 | 173 | /** @test */ 174 | function it_can_delete_itself() 175 | { 176 | $post_id = $this->factory()->post->create(); 177 | $model = Post::fromID($post_id); 178 | 179 | $this->assertSame($post_id, $model->id); 180 | $this->assertInstanceOf(WP_Post::class, get_post($post_id)); 181 | 182 | $model->delete(); 183 | 184 | $this->assertNull(get_post($post_id)); 185 | $this->assertNull($this->post); 186 | } 187 | 188 | 189 | /** @test */ 190 | function it_handles_trashing_and_untrashing() 191 | { 192 | $model = Post::create([ 193 | 'post_title' => 'Yay, I\'m Alive!', 194 | 'post_status' => 'publish' 195 | ]); 196 | 197 | $this->assertEquals('publish', get_post_status($model->id)); 198 | 199 | $model->trash(); 200 | 201 | $this->assertEquals('trash', get_post_status($model->id)); 202 | 203 | $model->untrash(); 204 | 205 | $this->assertEquals('publish', get_post_status($model->id)); 206 | } 207 | 208 | /** @test */ 209 | function it_has_a_method_for_refreshing_the_wrapped_post() 210 | { 211 | $model = Post::create([ 212 | 'post_title' => 'OG Title' 213 | ]); 214 | 215 | // the post is modified elsewhere 216 | wp_update_post([ 217 | 'ID' => $model->id, 218 | 'post_title' => 'Changed' 219 | ]); 220 | 221 | $this->assertEquals('OG Title', $model->post_title); 222 | $this->assertEquals('Changed', get_the_title($model->id)); 223 | 224 | $model->refresh(); 225 | 226 | $this->assertEquals('Changed', $model->post_title); 227 | } 228 | 229 | /** @test */ 230 | function it_can_save_changes_back_to_the_database() 231 | { 232 | $model = Post::create([ 233 | 'post_title' => 'OG Title' 234 | ]); 235 | 236 | $model->post_title = 'Changed'; 237 | 238 | $model->save(); 239 | 240 | $this->assertEquals('Changed', get_the_title($model->id)); 241 | } 242 | 243 | /** @test */ 244 | function it_offers_static_methods_for_querying() 245 | { 246 | $this->assertInstanceOf(\Silk\Query\Builder::class, Post::query()); 247 | $this->assertInstanceOf(\Silk\Query\Builder::class, (new Post)->newQuery()); 248 | } 249 | 250 | /** @test */ 251 | function it_has_a_static_method_for_starting_a_new_query_for_all_posts_of_type() 252 | { 253 | $this->factory()->post->create_many(15); 254 | 255 | $this->assertCount(15, Post::all()->results()); 256 | } 257 | 258 | /** @test */ 259 | function it_proxies_non_existent_static_methods_to_the_builder() 260 | { 261 | $this->assertInstanceOf( 262 | \Silk\Query\Builder::class, 263 | Post::limit(1) 264 | ); 265 | } 266 | 267 | /** @test */ 268 | function it_has_a_method_for_the_permalink() 269 | { 270 | $post = $this->factory()->post->create_and_get(); 271 | $model = Post::make($post); 272 | 273 | $this->assertSame( 274 | get_permalink($post->ID), 275 | $model->url() 276 | ); 277 | } 278 | 279 | /** @test */ 280 | function it_has_a_method_for_soft_retrieving_the_model_by_its_primary_id() 281 | { 282 | $post_id = $this->factory()->post->create(); 283 | 284 | try { 285 | $model = Post::find($post_id); 286 | } catch (\Exception $e) { 287 | $this->fail("Exception thrown while finding post with ID $post_id. " . $e->getMessage()); 288 | } 289 | 290 | $this->assertEquals($post_id, $model->id); 291 | } 292 | 293 | /** @test */ 294 | function find_returns_null_if_the_model_cannot_be_found() 295 | { 296 | $post_id = 0; 297 | 298 | try { 299 | $model = Post::find($post_id); 300 | } catch (\Exception $e) { 301 | $this->fail("Exception thrown while finding post with ID $post_id. " . $e->getMessage()); 302 | } 303 | 304 | $this->assertNull($model); 305 | } 306 | 307 | } 308 | 309 | class CustomTypeStub extends Post 310 | { 311 | const POST_TYPE = 'cpt_stub'; 312 | 313 | public static function __register() 314 | { 315 | register_post_type(static::POST_TYPE); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /tests/unit/PostType/PostTypeAssertions.php: -------------------------------------------------------------------------------- 1 | assertTrue(post_type_exists($slug)); 12 | } 13 | 14 | /** 15 | * [assertPostTypeExists description] 16 | * @param [type] $slug [description] 17 | */ 18 | protected function assertPostTypeNotExists($slug) 19 | { 20 | $this->assertFalse(post_type_exists($slug)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/unit/PostType/PostTypeBuilderTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Builder::class, Builder::make('new-type')); 19 | } 20 | 21 | /** @test */ 22 | function it_can_register_the_post_type() 23 | { 24 | $this->assertPostTypeNotExists('some-post-type'); 25 | 26 | Builder::make('some-post-type')->register(); 27 | 28 | $this->assertPostTypeExists('some-post-type'); 29 | } 30 | 31 | /** 32 | * @test 33 | * @expectedException Silk\PostType\Exception\InvalidPostTypeNameException 34 | */ 35 | function it_blows_up_if_the_post_type_slug_is_too_long() 36 | { 37 | Builder::make('twenty-character-limit')->register(); 38 | } 39 | 40 | /** 41 | * @test 42 | * @expectedException Silk\PostType\Exception\InvalidPostTypeNameException 43 | */ 44 | function it_blows_up_if_the_post_type_slug_is_too_short() 45 | { 46 | Builder::make('')->register(); 47 | } 48 | 49 | /** @test */ 50 | function it_accepts_an_array_or_parameters_for_supported_features() 51 | { 52 | Builder::make('bread')->supports(['flour', 'water'])->register(); 53 | 54 | $this->assertTrue(post_type_supports('bread', 'flour')); 55 | $this->assertTrue(post_type_supports('bread', 'water')); 56 | 57 | Builder::make('butter')->supports('bread', 'spreading')->register(); 58 | 59 | $this->assertTrue(post_type_supports('butter', 'bread')); 60 | $this->assertTrue(post_type_supports('butter', 'spreading')); 61 | } 62 | 63 | /** @test */ 64 | function it_can_get_and_set_arbitrary_values_for_the_registration_arguments() 65 | { 66 | $type = Builder::make('stuff') 67 | ->set('mood', 'happy') 68 | ->register(); 69 | 70 | $object = get_post_type_object('stuff'); 71 | 72 | $this->assertSame('happy', $object->mood); 73 | } 74 | 75 | /** @test */ 76 | function it_has_dedicated_methods_for_public_visibility() 77 | { 78 | $public = Builder::make('a-public-type')->open(); 79 | $this->assertTrue($public->get('public')); 80 | 81 | $private = Builder::make('a-private-type')->closed(); 82 | $this->assertFalse($private->get('public')); 83 | } 84 | 85 | /** @test */ 86 | function it_has_dedicated_methods_for_user_interface() 87 | { 88 | $ui = Builder::make('ui-having')->withUI(); 89 | $this->assertTrue($ui->get('show_ui')); 90 | 91 | $no_ui = Builder::make('no-ui')->noUI(); 92 | $this->assertFalse($no_ui->get('show_ui')); 93 | } 94 | 95 | /** @test */ 96 | function it_has_methods_for_setting_the_labels() 97 | { 98 | Builder::make('book') 99 | // override a default value 100 | ->setLabel('archives', 'All the Bookz') 101 | // override a default with a new placeholder 102 | ->setLabel('search_items', 'Find {many}') 103 | // set a non-standard label 104 | ->setLabel('some_custom_label', 'BOOKMADNESS') 105 | // populate singular defaults 106 | ->oneIs('Book') 107 | // populate plural defaults 108 | ->manyAre('Books') 109 | ->register(); 110 | 111 | $book = get_post_type_object('book'); 112 | $labels = get_post_type_labels($book); 113 | 114 | $this->assertSame('Book', $labels->singular_name); 115 | $this->assertSame('Books', $labels->name); 116 | $this->assertSame('All Books', $labels->all_items); 117 | $this->assertSame('Edit Book', $labels->edit_item); 118 | $this->assertSame('Find Books', $labels->search_items); 119 | $this->assertSame('BOOKMADNESS', $labels->some_custom_label); 120 | $this->assertSame('Add New Book', $labels->add_new_item); 121 | $this->assertSame('All the Bookz', $labels->archives); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/unit/PostType/PostTypeTest.php: -------------------------------------------------------------------------------- 1 | assertSame('post', $postType->id()); 16 | } 17 | 18 | /** 19 | * @test 20 | * @expectedException \InvalidArgumentException 21 | */ 22 | function it_blows_up_if_constructed_without_the_proper_type() 23 | { 24 | new PostType(new WP_Term(new stdClass)); 25 | } 26 | 27 | /** @test */ 28 | function it_has_a_named_constructor_for_creating_a_new_instance_from_an_existing_post_type() 29 | { 30 | $this->assertInstanceOf(PostType::class, PostType::load('post')); 31 | $this->assertInstanceOf(PostType::class, PostType::make('page')); 32 | } 33 | 34 | /** 35 | * @test 36 | * @expectedException Silk\PostType\Exception\NonExistentPostTypeException 37 | */ 38 | function it_blows_up_if_loading_a_non_existent_post_type() 39 | { 40 | PostType::load('non-existent-type'); 41 | } 42 | 43 | /** @test */ 44 | function the_make_method_returns_a_new_instance_for_existing_types_otherwise_a_builder_instance() 45 | { 46 | $this->assertInstanceOf(PostType::class, PostType::make('post')); 47 | $this->assertInstanceOf(Builder::class, PostType::make('mega-post')); 48 | } 49 | 50 | /** @test */ 51 | function it_can_unregister_the_post_type() 52 | { 53 | $type = PostType::make('temporary')->register(); 54 | 55 | $this->assertPostTypeExists('temporary'); 56 | 57 | $type->unregister(); 58 | 59 | $this->assertPostTypeNotExists('temporary'); 60 | } 61 | 62 | /** 63 | * @test 64 | * @expectedException Silk\PostType\Exception\NonExistentPostTypeException 65 | */ 66 | function it_blows_up_if_it_tries_to_unregister_a_nonexistent_type() 67 | { 68 | $type = PostType::make('non-existent')->register(); 69 | 70 | unregister_post_type('non-existent'); 71 | 72 | $type->unregister(); 73 | } 74 | 75 | /** 76 | * @test 77 | * @expectedException Silk\Exception\WP_ErrorException 78 | */ 79 | function it_blows_up_if_it_tries_to_unregister_a_built_in_post_type() 80 | { 81 | PostType::load('post')->unregister(); 82 | } 83 | 84 | /** @test */ 85 | function it_can_check_if_the_post_type_exists() 86 | { 87 | $this->assertTrue(PostType::exists('post')); 88 | $this->assertFalse(PostType::exists('post-it-note')); 89 | } 90 | 91 | /** @test */ 92 | function it_has_methods_for_adding_and_removing_support_for_post_type_features() 93 | { 94 | $type = PostType::load('post') 95 | ->addSupportFor('dollars', 'cents'); 96 | 97 | $this->assertTrue($type->supports('title', 'editor')); 98 | $this->assertFalse($type->supports('euros')); 99 | 100 | $type->removeSupportFor('dollars', 'cents')->addSupportFor('euros'); 101 | 102 | $this->assertFalse($type->supports('bits-of-string', 'euros', 'monopoly-money')); 103 | $this->assertTrue($type->supports('euros')); 104 | } 105 | 106 | /** @test */ 107 | function it_has_readonly_magic_properties() 108 | { 109 | $type = PostType::load('post'); 110 | 111 | $this->assertSame('post', $type->slug); 112 | $this->assertSame('Post', $type->one); 113 | $this->assertSame('Posts', $type->many); 114 | 115 | $this->assertNull($type->nonExistentProperty); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /tests/unit/Shortcode/ShortcodeTest.php: -------------------------------------------------------------------------------- 1 | assertSame(do_shortcode('[one]'), 'SomeShortcode::handler'); 15 | $this->assertSame(do_shortcode('[two]'), 'SomeShortcode::handler'); 16 | } 17 | 18 | /** @test */ 19 | function there_is_a_method_naming_convention_for_a_dedicated_handler_method() 20 | { 21 | SomeShortcode::register('foo'); 22 | $this->assertSame(do_shortcode('[foo]'), 'bar'); 23 | } 24 | 25 | /** @test */ 26 | function there_is_a_method_for_getting_the_attributes_as_a_collection() 27 | { 28 | $shortcode = new SomeShortcode(['test' => 'ok'], '', 'testing'); 29 | 30 | $this->assertInstanceOf(Collection::class, $shortcode->attributes()); 31 | $this->assertSame('ok', $shortcode->attributes()->get('test')); 32 | $this->assertCount(1, $shortcode->attributes()); 33 | } 34 | 35 | /** @test */ 36 | function it_returns_an_emtpy_string_if_no_handler_is_implemented() 37 | { 38 | $shortcode = new TestShortcode([], '', 'test'); 39 | $this->assertSame('', $shortcode->render()); 40 | } 41 | 42 | } 43 | 44 | /** 45 | * The parent Shortcode class is abstract, 46 | * so we use TestShortcode to test the unchanged behavior. 47 | */ 48 | class TestShortcode extends Shortcode {} 49 | 50 | class SomeShortcode extends Shortcode 51 | { 52 | public function handler() 53 | { 54 | return __METHOD__; 55 | } 56 | 57 | public function foo_handler() 58 | { 59 | return 'bar'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/unit/Support/CallbackTest.php: -------------------------------------------------------------------------------- 1 | assertSame(['CallStatic', 'method'], $callback->get()); 19 | } 20 | 21 | /** @test */ 22 | function it_has_a_method_for_reflecting_the_wrapped_callback() 23 | { 24 | $callback = new Callback(function () {}); 25 | 26 | $this->assertInstanceOf(ReflectionFunctionAbstract::class, $callback->reflect()); 27 | } 28 | 29 | /** @test */ 30 | function it_has_methods_to_call_the_wrapped_callback() 31 | { 32 | $callback = new Callback(function ($result = 'success') { 33 | return $result; 34 | }); 35 | 36 | $this->assertEquals('success', $callback->call()); 37 | $this->assertEquals('pass', $callback->call('pass')); 38 | 39 | $sumCallback = new Callback(function ($a, $b, $c) { 40 | return $a + $b + $c; 41 | }); 42 | 43 | $this->assertEquals(12, $sumCallback->callArray([4, 10, -2])); 44 | } 45 | 46 | /** @test */ 47 | function it_has_a_method_for_returning_the_number_of_parameters_accepted() 48 | { 49 | $this->assertEquals(0, (new Callback(function () {}))->parameterCount()); 50 | $this->assertEquals(1, (new Callback(function ($first) {}))->parameterCount()); 51 | $this->assertEquals(2, (new Callback(function ($first, $second) {}))->parameterCount()); 52 | } 53 | } 54 | 55 | class CallStatic 56 | { 57 | public static function method() 58 | { 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/unit/Taxonomy/TaxonomyBuilderTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Builder::class, Builder::make('new_tax')); 18 | } 19 | 20 | /** 21 | * @test 22 | * @expectedException Silk\Taxonomy\Exception\InvalidTaxonomyNameException 23 | */ 24 | function it_blows_up_if_the_taxononmy_name_is_too_short() 25 | { 26 | Builder::make('')->register(); 27 | } 28 | 29 | /** 30 | * @test 31 | * @expectedException Silk\Taxonomy\Exception\InvalidTaxonomyNameException 32 | */ 33 | function it_blows_up_if_the_taxononmy_name_is_too_long() 34 | { 35 | Builder::make('thisismorethanthirtytwocharacters')->register(); 36 | } 37 | 38 | /** @test */ 39 | function it_returns_a_new_taxonomy_instance_after_registering() 40 | { 41 | $registered = Builder::make('new_tax')->register(); 42 | 43 | $this->assertInstanceOf(Taxonomy::class, $registered); 44 | $this->assertSame('new_tax', $registered->id); 45 | } 46 | 47 | /** @test */ 48 | function it_has_methods_for_setting_the_labels() 49 | { 50 | $registered = Builder::make('genre') 51 | ->oneIs('Genre') 52 | ->manyAre('Genres') 53 | ->register(); 54 | 55 | $this->assertSame('All Genres', $registered->labels->all_items); 56 | } 57 | 58 | /** @test */ 59 | function it_registers_the_taxonomy_for_the_given_types() 60 | { 61 | Builder::make('new_tax') 62 | ->forTypes('post') 63 | ->register(); 64 | 65 | $this->assertObjectHasTaxonomy('post', 'new_tax'); 66 | } 67 | 68 | protected function assertObjectHasTaxonomy($object, $taxonomy) 69 | { 70 | $taxonomies = get_object_taxonomies($object); 71 | 72 | $this->assertContains($taxonomy, $taxonomies, print_r($taxonomies, true)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/unit/Taxonomy/TaxonomyTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(Taxonomy::exists('category')); 15 | $this->assertTrue(Taxonomy::exists('post_tag')); 16 | $this->assertFalse(Taxonomy::exists('non-existent')); 17 | } 18 | 19 | /** @test */ 20 | function it_takes_the_taxonomy_object_to_construct() 21 | { 22 | $object = get_taxonomy('category'); 23 | $taxonomy = new Taxonomy($object); 24 | 25 | $this->assertSame('category', $taxonomy->id()); 26 | } 27 | 28 | /** @test */ 29 | function it_has_a_named_constructor_which_takes_the_taxonomy_identifier() 30 | { 31 | $this->assertInstanceOf(Taxonomy::class, Taxonomy::make('category')); 32 | } 33 | 34 | /** 35 | * @test 36 | * @expectedException \Silk\Taxonomy\Exception\NonExistentTaxonomyException 37 | */ 38 | function it_blows_up_if_constructed_with_a_nonexistent_taxonomy() 39 | { 40 | new Taxonomy(get_taxonomy('non-existent')); 41 | } 42 | 43 | /** 44 | * @test 45 | * @expectedException \Silk\Taxonomy\Exception\NonExistentTaxonomyException 46 | */ 47 | function it_blows_up_when_attempting_to_load_an_unregistered_taxonomy() 48 | { 49 | Taxonomy::load('boom'); 50 | } 51 | 52 | /** 53 | * @test 54 | * @expectedException \Silk\Taxonomy\Exception\InvalidTaxonomyNameException 55 | */ 56 | function it_blows_up_if_the_taxononmy_name_is_too_short() 57 | { 58 | Taxonomy::make(''); 59 | } 60 | 61 | /** 62 | * @test 63 | * @expectedException \Silk\Taxonomy\Exception\InvalidTaxonomyNameException 64 | */ 65 | function it_blows_up_if_the_taxononmy_name_is_too_long() 66 | { 67 | Taxonomy::make('thisismorethanthirtytwocharacters'); 68 | } 69 | 70 | /** @test */ 71 | function it_can_unregister_the_taxonomy() 72 | { 73 | $this->assertFalse(Taxonomy::exists('temp')); 74 | 75 | register_taxonomy('temp', []); 76 | 77 | $this->assertTrue(Taxonomy::exists('temp')); 78 | 79 | $taxonomy = new Taxonomy(get_taxonomy('temp')); 80 | $taxonomy->unregister(); 81 | 82 | $this->assertFalse(Taxonomy::exists('temp')); 83 | } 84 | 85 | /** 86 | * @test 87 | * @expectedException \Silk\Taxonomy\Exception\NonExistentTaxonomyException 88 | */ 89 | function it_blows_up_if_trying_to_unregister_a_nonexistent_taxonomy() 90 | { 91 | register_taxonomy('temp', []); 92 | 93 | $taxonomy = new Taxonomy(get_taxonomy('temp')); 94 | 95 | unregister_taxonomy('temp'); 96 | 97 | $taxonomy->unregister(); 98 | } 99 | 100 | /** 101 | * @test 102 | * @expectedException Silk\Exception\WP_ErrorException 103 | */ 104 | function it_blows_up_if_attempting_to_unregister_a_builtin_taxonomy() 105 | { 106 | $taxonomy = new Taxonomy(get_taxonomy('category')); 107 | $taxonomy->unregister(); 108 | } 109 | 110 | /** @test */ 111 | function it_has_a_method_for_fetching_terms() 112 | { 113 | $this->assertInstanceOf(\Silk\Query\Builder::class, Taxonomy::make('category')->terms()); 114 | } 115 | 116 | /** @test */ 117 | function it_proxies_properties_to_the_taxonomy_object() 118 | { 119 | $model = Taxonomy::make('category'); 120 | 121 | $this->assertSame('Categories', $model->label); 122 | } 123 | 124 | /** @test */ 125 | function it_has_readonly_properties() 126 | { 127 | $model = Taxonomy::make('category'); 128 | 129 | $this->assertSame('category', $model->id); 130 | } 131 | 132 | /** @test */ 133 | function it_can_return_a_collection_of_post_types_associated_with_it() 134 | { 135 | register_taxonomy('breed', ['dog', 'cat']); 136 | register_post_type('dog', ['taxonomies' => (array) 'breed']); 137 | register_post_type('cat', ['taxonomies' => (array) 'breed']); 138 | 139 | $types = Taxonomy::make('breed')->postTypes(); 140 | 141 | $this->assertInstanceOf(Collection::class, $types); 142 | $this->assertCount(2, $types); 143 | $this->assertContains('dog', $types->pluck('id')); 144 | $this->assertContains('cat', $types->pluck('id')); 145 | } 146 | 147 | /** @test */ 148 | function it_has_readonly_magic_properties() 149 | { 150 | $type = Taxonomy::make('category'); 151 | 152 | $this->assertSame('category', $type->slug); 153 | $this->assertSame('Category', $type->one); 154 | $this->assertSame('Categories', $type->many); 155 | 156 | $this->assertNull($type->nonExistentProperty); 157 | } 158 | 159 | /** @test */ 160 | function non_existent_properties_return_null() 161 | { 162 | $this->assertNull(Taxonomy::make('category')->non_existent); 163 | } 164 | 165 | /** @test */ 166 | function it_returns_a_new_builder_for_its_taxonomy_if_not_registered_yet() 167 | { 168 | $this->assertInstanceOf(Builder::class, Taxonomy::make('non_existent_tax')); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /tests/unit/Term/TermQueryBuilderTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Collection::class, $query->results()); 17 | } 18 | 19 | /** @test */ 20 | function it_can_limit_the_results_to_a_given_taxonomy() 21 | { 22 | /** 23 | * By default get_terms does not include any that are not assigned to a post. 24 | * 25 | * We will limit the query to tags, but to be sure, assign all terms to a post. 26 | */ 27 | $post_id = $this->factory()->post->create(); 28 | $this->createManyTagsForPost(3, $post_id); 29 | $this->createManyCatsForPost(3, $post_id); 30 | 31 | $results = (new QueryBuilder) 32 | ->forTaxonomy('post_tag') 33 | ->results(); 34 | 35 | $this->assertCount(3, $results); 36 | $this->assertSame(['post_tag','post_tag','post_tag'], $results->pluck('taxonomy')->all()); 37 | } 38 | 39 | /** @test */ 40 | function it_can_include_unattached_terms() 41 | { 42 | $post_id = $this->factory()->post->create(); 43 | $this->createManyTags(3); // empties 44 | $this->createManyTagsForPost(3, $post_id); // assigned 45 | 46 | $query = (new QueryBuilder) 47 | ->forTaxonomy('post_tag') 48 | ->includeEmpty(); 49 | 50 | $results = $query->results(); 51 | 52 | $this->assertCount(6, $results); 53 | } 54 | 55 | /** @test */ 56 | function it_can_query_all_terms() 57 | { 58 | $post_id = $this->factory()->post->create(); 59 | 60 | $this->createManyTags(2); // empties 61 | $this->createManyTagsForPost(3, $post_id); // assigned 62 | 63 | $tags = (new QueryBuilder)->forTaxonomy('post_tag')->all()->results(); 64 | $this->assertCount(2 + 3, $tags); 65 | 66 | $this->createManyCats(4); // empties 67 | $this->createManyCatsForPost(5, $post_id); // assigned 68 | 69 | $cats = (new QueryBuilder)->forTaxonomy('category')->all()->results(); 70 | // +1 cat for Uncategorized 71 | $this->assertCount(4 + 5 + 1, $cats); 72 | 73 | $alls = (new QueryBuilder)->all()->results(); 74 | $this->assertCount(2 + 3 + 4 + 5 + 1, $alls); 75 | } 76 | 77 | /** @test */ 78 | function it_can_limit_the_maximum_number_of_results_to_a_given_number() 79 | { 80 | $this->createManyTags(7); 81 | 82 | $query = (new QueryBuilder) 83 | ->includeEmpty() 84 | ->limit(5); 85 | 86 | $this->assertCount(5, $query->results()); 87 | } 88 | 89 | /** 90 | * @test 91 | * @expectedException Silk\Exception\WP_ErrorException 92 | */ 93 | function it_blows_up_if_trying_to_query_terms_of_a_non_taxonomy() 94 | { 95 | (new QueryBuilder) 96 | ->forTaxonomy('non-existent') 97 | ->results(); 98 | } 99 | 100 | /** @test */ 101 | function it_can_accept_and_return_a_model() 102 | { 103 | $model = new Category; 104 | $builder = (new QueryBuilder)->setModel($model); 105 | 106 | $this->assertSame($model, $builder->getModel()); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /tests/unit/Term/TermTest.php: -------------------------------------------------------------------------------- 1 | name = 'Red'; 20 | $model->save(); 21 | 22 | $term = get_term_by('name', 'Red', 'category'); 23 | 24 | $this->assertSame($model->id, $term->term_id); 25 | } 26 | 27 | /** @test */ 28 | function it_can_be_instantiated_with_an_array_of_attributes() 29 | { 30 | $model = new Category([ 31 | 'name' => 'Blue' 32 | ]); 33 | 34 | $this->assertSame('Blue', $model->object->name); 35 | $this->assertSame('Blue', $model->name); 36 | } 37 | 38 | /** @test */ 39 | function it_can_create_a_new_instance_from_a_wp_term() 40 | { 41 | wp_insert_term('Blue', 'category'); 42 | $term = get_term_by('name', 'Blue', 'category'); 43 | 44 | $model = new Category($term); 45 | 46 | $this->assertInstanceOf(Category::class, $model); 47 | $this->assertSame($term->term_id, $model->id); 48 | } 49 | 50 | /** @test */ 51 | function it_can_create_a_new_instance_from_a_term_slug() 52 | { 53 | wp_insert_term('Green', 'category'); 54 | 55 | $model = Category::fromSlug('green'); 56 | 57 | $term = get_term_by('slug', 'green', 'category'); 58 | 59 | $this->assertSame($term->term_id, $model->id); 60 | } 61 | 62 | /** 63 | * @test 64 | * @expectedException \Silk\Term\Exception\TermNotFoundException 65 | */ 66 | function it_blows_up_if_the_term_cannot_be_found_by_slug() 67 | { 68 | Category::fromSlug('non-existent-slug'); 69 | } 70 | 71 | /** 72 | * @test 73 | * @expectedException \Silk\Term\Exception\TaxonomyMismatchException 74 | */ 75 | function it_blows_up_if_the_terms_taxonomy_does_not_match_the_models() 76 | { 77 | wp_insert_term('Green', 'post_tag'); 78 | $tag_term = get_term_by('name', 'Green', 'post_tag'); 79 | 80 | new Category($tag_term); 81 | } 82 | 83 | /** @test */ 84 | function it_can_create_a_new_instance_from_a_term_id() 85 | { 86 | $ids = wp_insert_term('Purple', 'category'); 87 | 88 | $model = Category::fromID($ids['term_id']); 89 | 90 | $this->assertInstanceOf(Category::class, $model); 91 | $this->assertSame($ids['term_id'], $model->id); 92 | } 93 | 94 | /** 95 | * @test 96 | * @expectedException \Silk\Term\Exception\TermNotFoundException 97 | */ 98 | function it_blows_up_if_the_term_cannot_be_found_by_id() 99 | { 100 | Category::fromID(0); 101 | } 102 | 103 | /** @test */ 104 | function it_has_a_named_constructor_for_creating_a_new_instance_and_term_at_the_same_time() 105 | { 106 | $model = Category::create([ 107 | 'name' => 'Carnivore', 108 | 'slug' => 'meat-eater', 109 | ]); 110 | 111 | $term = get_term_by('slug', 'meat-eater', 'category'); 112 | 113 | $this->assertSame($term->term_id, $model->id); 114 | $this->assertSame('meat-eater', $model->slug); 115 | } 116 | 117 | /** @test */ 118 | function it_has_method_for_checking_if_the_term_exists() 119 | { 120 | $model = new Category; 121 | $this->assertFalse($model->exists()); 122 | 123 | $model->name = 'Alive'; 124 | $model->save(); 125 | 126 | $this->assertTrue($model->exists()); 127 | } 128 | 129 | /** @test */ 130 | function it_has_a_method_for_checking_if_the_term_is_a_child_of_another_term() 131 | { 132 | $parent = Category::create([ 133 | 'name' => 'Parent' 134 | ]); 135 | 136 | $child = Category::create([ 137 | 'name' => 'Child', 138 | 'parent' => $parent->id 139 | ]); 140 | 141 | $this->assertTrue($child->isChildOf($parent->id)); 142 | $this->assertTrue($child->isChildOf($parent)); 143 | } 144 | 145 | /** @test */ 146 | function it_can_save_changes_to_the_database() 147 | { 148 | $model = Category::create(['name' => 'Initial Name']); 149 | 150 | $this->assertSame( 151 | 'Initial Name', 152 | get_term_field('name', $model->id, $model->taxonomy) 153 | ); 154 | 155 | $model->name = 'New Name'; 156 | $model->save(); 157 | 158 | $this->assertSame( 159 | 'New Name', 160 | get_term_field('name', $model->id, $model->taxonomy) 161 | ); 162 | } 163 | 164 | /** 165 | * @test 166 | * @expectedException Silk\Exception\WP_ErrorException 167 | */ 168 | function it_blows_up_if_trying_to_save_a_term_without_a_name() 169 | { 170 | $model = new Category; 171 | $model->save(); 172 | } 173 | 174 | /** @test */ 175 | function it_can_delete_itself() 176 | { 177 | $model = Category::create(['name' => 'Doomed']); 178 | $this->assertTrue($model->exists()); 179 | 180 | $model->delete(); 181 | 182 | $this->assertFalse($model->exists()); 183 | $this->assertEmpty($model->id); 184 | $this->assertEmpty($model->term_taxonomy_id); 185 | } 186 | 187 | /** @test */ 188 | function it_blows_up_if_it_tries_to_delete_a_non_existent_term() 189 | { 190 | $model = new Category; // does not exist yet 191 | $model->delete(); 192 | } 193 | 194 | /** @test */ 195 | function that_non_existent_properties_return_null() 196 | { 197 | $model = new Category; 198 | $this->assertNull($model->non_existent_property); 199 | } 200 | 201 | /** @test */ 202 | function it_reports_proxied_properties_as_set() 203 | { 204 | $model = new Category; 205 | 206 | $this->assertTrue(isset($model->name)); 207 | $this->assertTrue(isset($model->slug)); 208 | $this->assertTrue(isset($model->taxonomy)); 209 | } 210 | 211 | /** @test */ 212 | function it_has_a_method_for_returning_the_parent_instance() 213 | { 214 | $parent = Category::create([ 215 | 'name' => 'Parent' 216 | ]); 217 | 218 | $child = Category::create([ 219 | 'name' => 'Child', 220 | 'parent' => $parent->id 221 | ]); 222 | 223 | $this->assertSame($child->parent, $child->parent()->id); 224 | } 225 | 226 | /** @test */ 227 | function it_can_get_all_of_its_ancestors_as_model_instances_of_the_same_class() 228 | { 229 | $grand = Category::create([ 230 | 'name' => 'Grandparent' 231 | ]); 232 | $parent = Category::create([ 233 | 'name' => 'Parent', 234 | 'parent' => $grand->id 235 | ]); 236 | $child = Category::create([ 237 | 'name' => 'Child', 238 | 'parent' => $parent->id 239 | ]); 240 | 241 | $ancestors = $child->ancestors(); 242 | 243 | $this->assertCount(2, $ancestors); 244 | $this->assertInstanceOf(Category::class, $ancestors[0]); 245 | $this->assertInstanceOf(Category::class, $ancestors[1]); 246 | $this->assertSame($parent->id, $ancestors[0]->id); 247 | $this->assertSame($grand->id, $ancestors[1]->id); 248 | } 249 | 250 | /** @test */ 251 | function it_can_get_all_of_its_children_as_model_instances_of_the_same_class() 252 | { 253 | $grand = Category::create([ 254 | 'name' => 'Grandparent' 255 | ]); 256 | $parent = Category::create([ 257 | 'name' => 'Parent', 258 | 'parent' => $grand->id 259 | ]); 260 | $child = Category::create([ 261 | 'name' => 'Child', 262 | 'parent' => $parent->id 263 | ]); 264 | 265 | $children = $grand->children(); 266 | 267 | $this->assertCount(2, $children); 268 | $this->assertInstanceOf(Category::class, $children[0]); 269 | $this->assertInstanceOf(Category::class, $children[1]); 270 | $this->assertSame($parent->id, $children[0]->id); 271 | $this->assertSame($child->id, $children[1]->id); 272 | } 273 | 274 | /** @test */ 275 | function it_can_query_terms_of_the_same_type() 276 | { 277 | $post_id = $this->factory()->post->create(); 278 | 279 | $this->createManyTagsForPost(5, $post_id); 280 | 281 | $tags = Tag::query()->results(); 282 | 283 | $this->assertCount(5, $tags); 284 | 285 | foreach ($tags as $tag) { 286 | $this->assertInstanceOf(Tag::class, $tag); 287 | } 288 | } 289 | 290 | /** @test */ 291 | function it_has_a_method_for_returning_the_taxonomy_model() 292 | { 293 | $term = new Category; 294 | 295 | $this->assertInstanceOf(Taxonomy::class, $term->taxonomy()); 296 | $this->assertSame('category', $term->taxonomy()->id); 297 | } 298 | 299 | /** @test */ 300 | function it_has_a_method_for_accessing_the_meta_api() 301 | { 302 | $model = Category::create(['name' => 'Testing']); 303 | 304 | $this->assertInstanceOf(ObjectMeta::class, $model->meta()); 305 | $this->assertInstanceOf(Meta::class, $model->meta('some-key')); 306 | 307 | $model->meta('some-key')->set('single value'); 308 | 309 | $this->assertSame('single value', get_term_meta($model->id, 'some-key', true)); 310 | } 311 | 312 | /** @test */ 313 | function it_returns_a_new_builder_for_its_taxonomy_if_not_registered_yet() 314 | { 315 | $this->assertInstanceOf(Builder::class, NewTerm::taxonomy()); 316 | } 317 | 318 | /** @test */ 319 | function it_has_a_method_for_getting_the_term_archive_url() 320 | { 321 | $model = $model = Category::create(['name' => 'Awesome']); 322 | 323 | $this->assertSame( 324 | get_term_link($model->id, $model->taxonomy), 325 | $model->url() 326 | ); 327 | } 328 | 329 | /** 330 | * @test 331 | * @expectedException Silk\Exception\WP_ErrorException 332 | */ 333 | function it_blows_up_if_getting_a_term_url_for_a_non_existent_term() 334 | { 335 | (new Category)->url(); 336 | } 337 | 338 | /** @test */ 339 | function it_has_a_method_for_soft_retrieving_the_model_by_its_primary_id() 340 | { 341 | $term_id = $this->factory()->category->create(); 342 | 343 | try { 344 | $model = Category::find($term_id); 345 | } catch (\Exception $e) { 346 | $this->fail("Exception thrown while finding term with ID $term_id. " . $e->getMessage()); 347 | } 348 | 349 | $this->assertEquals($term_id, $model->id); 350 | } 351 | 352 | /** @test */ 353 | function find_returns_null_if_the_model_cannot_be_found() 354 | { 355 | $term_id = 0; 356 | 357 | try { 358 | $model = Category::find($term_id); 359 | } catch (\Exception $e) { 360 | $this->fail("Exception thrown while finding term with ID $term_id. " . $e->getMessage()); 361 | } 362 | 363 | $this->assertNull($model); 364 | } 365 | } 366 | 367 | class NewTerm extends Model 368 | { 369 | const TAXONOMY = 'new_taxonomy'; 370 | } 371 | -------------------------------------------------------------------------------- /tests/unit/User/UserModelTest.php: -------------------------------------------------------------------------------- 1 | assertSame($blankUser, $model->object); 22 | 23 | $modelFromAtts = new User([ 24 | 'user_login' => 'z3r0c00l', 25 | 'user_pass' => 'iheartkate' 26 | ]); 27 | 28 | $this->assertSame('z3r0c00l', $modelFromAtts->object->user_login); 29 | $this->assertSame('iheartkate', $modelFromAtts->object->user_pass); 30 | } 31 | 32 | /** @test */ 33 | function it_can_create_a_new_instance_from_a_user_id() 34 | { 35 | $user_id = $this->factory()->user->create(); 36 | 37 | $model = User::fromID($user_id); 38 | 39 | $this->assertInstanceOf(User::class, $model); 40 | $this->assertSame($user_id, $model->id); 41 | } 42 | 43 | /** 44 | * @test 45 | * @expectedException Silk\User\Exception\UserNotFoundException 46 | */ 47 | function it_blows_up_if_unable_to_locate_a_user_by_id() 48 | { 49 | User::fromID(0); 50 | } 51 | 52 | 53 | /** @test */ 54 | function it_can_create_a_new_instance_from_a_username() 55 | { 56 | $user = $this->factory()->user->create_and_get(); 57 | 58 | $model = User::fromUsername($user->user_login); 59 | 60 | $this->assertInstanceOf(User::class, $model); 61 | $this->assertSame($user->ID, $model->id); 62 | } 63 | 64 | /** 65 | * @test 66 | * @expectedException Silk\User\Exception\UserNotFoundException 67 | */ 68 | function it_blows_up_if_no_user_is_found_with_the_given_username() 69 | { 70 | User::fromUsername('non-existent-username'); 71 | } 72 | 73 | 74 | /** @test */ 75 | function it_can_create_a_new_instance_from_the_users_email_address() 76 | { 77 | $user = $this->factory()->user->create_and_get(); 78 | 79 | $model = User::fromEmail($user->user_email); 80 | 81 | $this->assertInstanceOf(User::class, $model); 82 | $this->assertSame($user->ID, $model->id); 83 | } 84 | 85 | /** 86 | * @test 87 | * @expectedException Silk\User\Exception\UserNotFoundException 88 | */ 89 | function it_blows_up_if_no_user_is_found_with_the_given_email() 90 | { 91 | User::fromEmail('non-existent@user.com'); 92 | } 93 | 94 | /** @test */ 95 | function it_can_create_a_new_instance_from_the_user_slug() 96 | { 97 | $user = $this->factory()->user->create_and_get(); 98 | 99 | $model = User::fromSlug($user->user_nicename); 100 | 101 | $this->assertInstanceOf(User::class, $model); 102 | $this->assertSame($user->ID, $model->id); 103 | } 104 | 105 | /** 106 | * @test 107 | * @expectedException Silk\User\Exception\UserNotFoundException 108 | */ 109 | function it_blows_up_if_no_user_is_found_with_the_given_slug() 110 | { 111 | User::fromSlug('non-existent'); 112 | } 113 | 114 | /** @test */ 115 | function it_can_create_a_new_user_from_a_new_instance() 116 | { 117 | $model = new User; 118 | $model->user_login = 'bigbird'; 119 | $model->user_pass = 'rub_a_dub_dub'; 120 | $model->save(); 121 | 122 | $this->assertNotEmpty($model->id, 123 | "Failed asserting that the User ID $model->id is > 0" 124 | ); 125 | } 126 | 127 | /** 128 | * @test 129 | * @expectedException \Silk\Exception\WP_ErrorException 130 | */ 131 | function it_blows_up_if_trying_to_create_a_user_without_a_username() 132 | { 133 | $model = new User; 134 | $model->user_login = ''; 135 | $model->user_pass = 'password'; 136 | $model->save(); 137 | } 138 | 139 | /** @test */ 140 | function it_can_update_an_existing_user() 141 | { 142 | $user = $this->factory()->user->create_and_get(); 143 | 144 | $model = new User($user); 145 | $model->first_name = 'Franky'; 146 | $model->last_name = 'Fivefingers'; 147 | $model->save(); 148 | 149 | $updated = new WP_User($user->ID); 150 | 151 | $this->assertSame('Franky', $updated->first_name); 152 | $this->assertSame('Fivefingers', $updated->last_name); 153 | } 154 | 155 | /** @test */ 156 | function it_can_delete_the_modeled_user() 157 | { 158 | $user = $this->factory()->user->create_and_get(); 159 | 160 | $model = new User($user); 161 | 162 | $model->delete(); 163 | 164 | $this->assertFalse(get_user_by('ID', $user->ID)); 165 | } 166 | 167 | /** @test */ 168 | function it_exposes_the_meta_api_for_users() 169 | { 170 | $user = $this->factory()->user->create_and_get(); 171 | 172 | $model = new User($user); 173 | 174 | $model->meta('position')->set('President'); 175 | $model->meta('salary')->set('big'); 176 | 177 | $this->assertSame('President', get_user_meta($user->ID, 'position', true)); 178 | $this->assertSame('big', get_user_meta($user->ID, 'salary', true)); 179 | 180 | update_user_meta($user->ID, 'favorite_food', 'pizza'); 181 | 182 | $this->assertSame('pizza', $model->favorite_food); 183 | } 184 | 185 | /** @test */ 186 | function the_query_method_fulfills_the_contract() 187 | { 188 | $this->assertInstanceOf(\Silk\Query\Builder::class, User::query()); 189 | } 190 | 191 | /** @test */ 192 | function it_has_a_method_to_get_the_url_for_the_users_posts() 193 | { 194 | $user = $this->factory()->user->create_and_get(['nicename' => 'franky']); 195 | $model = new User($user); 196 | 197 | $this->assertSame( 198 | get_author_posts_url($user->ID), 199 | $model->postsUrl() 200 | ); 201 | } 202 | 203 | /** @test */ 204 | function it_can_create_a_new_user() 205 | { 206 | $model = User::create([ 207 | 'user_login' => 'ralph', 208 | 'user_pass' => '123456' 209 | ]); 210 | 211 | $user = new WP_User($model->id); 212 | 213 | $this->assertSame($model->id, $user->ID); 214 | $this->assertSame('ralph', $user->user_login); 215 | } 216 | 217 | /** @test */ 218 | function it_can_create_a_new_instance_from_the_current_authenticated_user() 219 | { 220 | $user_id = $this->factory()->user->create(); 221 | wp_set_current_user($user_id); 222 | 223 | $model = User::auth(); 224 | 225 | $this->assertSame($user_id, $model->id); 226 | } 227 | 228 | /** @test */ 229 | function it_refreshes_the_user_object_on_save() 230 | { 231 | $model = new User; 232 | $model->user_login = 'tester'; 233 | $model->user_pass = 'password'; 234 | $model->save(); 235 | // Password is now hashed... 236 | $this->assertNotSame('password', $model->user_pass); 237 | $this->assertTrue(wp_check_password('password', $model->user_pass)); 238 | } 239 | 240 | /** @test */ 241 | function it_can_alias_some_properties_to_user_data() 242 | { 243 | $user = $this->factory()->user->create_and_get(); 244 | $model = new UserWithAliases($user); 245 | 246 | $this->assertSame($user->user_email, $model->email); 247 | $this->assertSame($user->user_nicename, $model->slug); 248 | $this->assertSame($user->user_login, $model->username); 249 | $this->assertSame($user->user_pass, $model->password); 250 | } 251 | 252 | /** @test */ 253 | function it_works_with_shorthand_too() 254 | { 255 | $model = new ShorthandUser([ 256 | 'login' => 'admin', 257 | 'pass' => '12345' 258 | ]); 259 | 260 | $this->assertSame('admin', $model->user_login); 261 | $this->assertSame('12345', $model->user_pass); 262 | unset($model); 263 | 264 | $model = new ShorthandUser(new WP_User); 265 | $model->login = 'helper'; 266 | $model->pass = '6789'; 267 | 268 | $this->assertSame('helper', $model->user_login); 269 | $this->assertSame('6789', $model->user_pass); 270 | } 271 | 272 | /** @test */ 273 | function it_has_a_method_for_soft_retrieving_the_model_by_its_primary_id() 274 | { 275 | $user_id = $this->factory()->user->create(); 276 | 277 | try { 278 | $model = User::find($user_id); 279 | } catch (\Exception $e) { 280 | $this->fail("Exception thrown while finding user with ID $user_id. " . $e->getMessage()); 281 | } 282 | 283 | $this->assertEquals($user_id, $model->id); 284 | } 285 | 286 | /** @test */ 287 | function find_returns_null_if_the_model_cannot_be_found() 288 | { 289 | $user_id = 0; 290 | 291 | try { 292 | $model = User::find($user_id); 293 | } catch (\Exception $e) { 294 | $this->fail("Exception thrown while finding user with ID $user_id. " . $e->getMessage()); 295 | } 296 | 297 | $this->assertNull($model); 298 | } 299 | } 300 | 301 | class UserWithAliases extends User 302 | { 303 | protected function objectAliases() { 304 | return [ 305 | 'email' => 'user_email', 306 | 'slug' => 'user_nicename', 307 | 'username' => 'user_login', 308 | 'password' => 'user_pass', 309 | ]; 310 | } 311 | } 312 | 313 | class ShorthandUser extends User 314 | { 315 | use ShorthandProperties; 316 | } 317 | -------------------------------------------------------------------------------- /tests/unit/User/UserQueryBuilderTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(QueryBuilder::class, new QueryBuilder(new WP_User_Query)); 13 | } 14 | 15 | /** @test */ 16 | function if_no_user_query_instance_is_provided_it_will_create_one_for_us() 17 | { 18 | $this->assertInstanceOf(QueryBuilder::class, new QueryBuilder); 19 | } 20 | 21 | /** @test */ 22 | function it_has_a_named_constructor_for_creating_a_new_instance() 23 | { 24 | $this->assertInstanceOf(QueryBuilder::class, QueryBuilder::make()); 25 | } 26 | 27 | /** @test */ 28 | function it_returns_the_results_as_a_collection() 29 | { 30 | $this->assertInstanceOf(Collection::class, QueryBuilder::make()->results()); 31 | } 32 | 33 | /** @test */ 34 | function it_can_take_and_return_a_user_model() 35 | { 36 | $builder = new QueryBuilder(); 37 | 38 | $user = new Model; 39 | $builder->setModel($user); 40 | 41 | $this->assertSame($user, $builder->getModel()); 42 | } 43 | 44 | /** @test */ 45 | function it_can_accept_arbitrary_query_vars() 46 | { 47 | $query = Mockery::spy(WP_User_Query::class); 48 | 49 | $builder = new QueryBuilder($query); 50 | $builder->set('count_total', false); 51 | 52 | $query->shouldHaveReceived('set')->with('count_total', false); 53 | } 54 | 55 | 56 | /** @test */ 57 | function it_returns_the_results_as_a_collection_of_model_instances_when_set() 58 | { 59 | $new_user_id = $this->factory()->user->create(); 60 | 61 | $builder = new QueryBuilder(); 62 | $builder->setModel(new Model); 63 | $results = $builder->results(); 64 | 65 | $this->assertInstanceOf(Model::class, $results->first()); 66 | $this->assertContains($new_user_id, $results->pluck('ID')); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /tests/wp-config.php: -------------------------------------------------------------------------------- 1 |