diff --git a/.env b/.env new file mode 100644 index 00000000..3cf017a0 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +DOCKER_HOST_NEO4J_HTTP_PORT=7474 +DOCKER_HOST_NEO4J_BOLT_PORT=7687 \ No newline at end of file diff --git a/.floo b/.floo deleted file mode 100644 index ea73e248..00000000 --- a/.floo +++ /dev/null @@ -1,3 +0,0 @@ -{ - "url": "https://floobits.com/Mulkave/NeoEloquent-GraphAware-php-client" -} \ No newline at end of file diff --git a/.flooignore b/.flooignore deleted file mode 100644 index ed824d39..00000000 --- a/.flooignore +++ /dev/null @@ -1,6 +0,0 @@ -extern -node_modules -tmp -vendor -.idea/workspace.xml -.idea/misc.xml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..9c0cacb6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Create client with neo4j scheme +2. Open transaction +3. Commit transaction +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - Library version: [e.g. 2.0.8, use `composer show -i laudis/neo4j-php-client` to find out] + - Neo4j Version: [e.g. 4.2.1, aura, use `neo4j version` to find out] + - PHP version: [e.g. 8.0.2, use `php -v` to find out] + - OS: [e.g. Linux, 5.13.4-1-MANJARO, Windows 10] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..11fc491e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/integration-test-single-server.yml b/.github/workflows/integration-test-single-server.yml new file mode 100644 index 00000000..3d15b584 --- /dev/null +++ b/.github/workflows/integration-test-single-server.yml @@ -0,0 +1,50 @@ +name: Integration Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + tests: + runs-on: ubuntu-latest + name: "Running on PHP 8.1 with a Neo4j 5.5 instance" + + services: + neo4j: + image: neo4j:5.5 + env: + NEO4J_AUTH: neo4j/testtest + NEO4JLABS_PLUGINS: '["apoc"]' + ports: + - 7687:7687 + - 7474:7474 + options: >- + --health-cmd "wget -q --method=HEAD http://localhost:7474 || exit 1" + --health-start-period "60s" + --health-interval "30s" + --health-timeout "15s" + --health-retries "5" + + steps: + - uses: actions/checkout@v2 + - name: Cache Composer dependencies + uses: actions/cache@v2 + with: + path: /tmp/composer-cache + key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }} + - uses: php-actions/composer@v6 + with: + progress: yes + php_version: 8.1 + version: 2 + - uses: php-actions/phpunit@v3 + with: + configuration: phpunit.xml.dist + php_version: 8.1 + version: 9 + testsuite: Integration + bootstrap: vendor/autoload.php diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 00000000..477adac2 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,28 @@ +name: Static Analysis + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + php-cs-fixer: + name: "Lint" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Cache Composer dependencies + uses: actions/cache@v2 + with: + path: /tmp/composer-cache + key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }} + - uses: php-actions/composer@v6 + with: + progress: yes + php_version: 8.0 + version: 2 + - name: "Laravel Pint" + run: vendor/bin/pint --test diff --git a/.gitignore b/.gitignore index 723c6633..e0198cc5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ composer.lock Examples/**/vendor .phpunit.result.cache +.phpunit.cache/ + +.psalm \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e1b345d6..00000000 --- a/.travis.yml +++ /dev/null @@ -1,40 +0,0 @@ -language: php - -php: - - 7.1 - -services: - - neo4j - -matrix: - allow_failures: - - env: NEO4J_VERSION="2.3" - -before_script: - # install dependencies to add repos - - sudo apt-get install -y python-software-properties - - sudo apt-get update - - #install openjdk for java - - sudo apt-get install openjdk-8-jdk - - # install composer - - travis_retry composer self-update - - travis_retry composer install --prefer-source --no-interaction - -script: vendor/bin/phpunit - -env: - global: - - NEO4J_AUTH=none - matrix: - - NEO4J_VERSION="2.3" - - NEO4J_VERSION="3.0" - - NEO4J_VERSION="3.1" - - NEO4J_VERSION="3.2" - - NEO4J_VERSION="3.3" - -notifications: - slack: - rooms: - - vinelab:52MiVOHdct34FRg2o9sPBlJJ#graphdb diff --git a/Dockerfile b/Dockerfile index 7c5ecee8..77870899 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,15 @@ -FROM php:8.0-alpine +FROM php:8.1-alpine -RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \ +RUN apk add --no-cache $PHPIZE_DEPS git linux-headers \ && pecl install xdebug \ - && docker-php-ext-enable xdebug \ - && apk del -f .build-deps + && docker-php-ext-enable xdebug RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer COPY composer.json composer.loc[k] ./ -RUN composer install --ignore-platform-reqs #temporary workaround as the bolt library incorrectly enforces the sockets extension +RUN composer install -COPY Examples/ ./ COPY src/ ./ COPY tests/ ./ -COPY phpunit.xml .travis.yml ./ \ No newline at end of file +COPY phpunit.xml ./ \ No newline at end of file diff --git a/Examples/Movies/README.md b/Examples/Movies/README.md deleted file mode 100644 index 53089090..00000000 --- a/Examples/Movies/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Movie - NeoEloquent Example -Illustrating the all-time favourite [movie example from the Neo4j docs](http://neo4j.com/docs/stable/cypherdoc-movie-database.html) - -### How to Run -- Start with running this Cypher in your database to fill it up with some data: - -```cypher -CREATE (matrix1:Movie { title : 'The Matrix', year : '1999-03-31' }) -CREATE (matrix2:Movie { title : 'The Matrix Reloaded', year : '2003-05-07' }) -CREATE (matrix3:Movie { title : 'The Matrix Revolutions', year : '2003-10-27' }) -CREATE (keanu:Actor { name:'Keanu Reeves' }) -CREATE (laurence:Actor { name:'Laurence Fishburne' }) -CREATE (carrieanne:Actor { name:'Carrie-Anne Moss' }) -CREATE (keanu)-[:ACTS_IN { role : 'Neo' }]->(matrix1) -CREATE (keanu)-[:ACTS_IN { role : 'Neo' }]->(matrix2) -CREATE (keanu)-[:ACTS_IN { role : 'Neo' }]->(matrix3) -CREATE (laurence)-[:ACTS_IN { role : 'Morpheus' }]->(matrix1) -CREATE (laurence)-[:ACTS_IN { role : 'Morpheus' }]->(matrix2) -CREATE (laurence)-[:ACTS_IN { role : 'Morpheus' }]->(matrix3) -CREATE (carrieanne)-[:ACTS_IN { role : 'Trinity' }]->(matrix1) -CREATE (carrieanne)-[:ACTS_IN { role : 'Trinity' }]->(matrix2) -CREATE (carrieanne)-[:ACTS_IN { role : 'Trinity' }]->(matrix3) -``` - -- In the terminal `cd` into this example's directory: `cd Examples/Movies` -- Run `composer install` -- Run `php start.php` - -### About the Code -The code is inside the `start.php` file. - -Using [composer](http://getcomposer.org) all classes inside `models/` are autoloaded so in case you wanted to play around with this example make sure to run `composer dump-autoload` after you add classes and before you run the example again. - -As for customizing configuration check `config/database.php` and modify the `$config` array as you wish. diff --git a/Examples/Movies/composer.json b/Examples/Movies/composer.json deleted file mode 100644 index 5b2f9d32..00000000 --- a/Examples/Movies/composer.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "neoeloquent-examples/movie", - "description": "Illustrating the all-time favourite movie example from the Neo4j docs: http://neo4j.com/docs/stable/cypherdoc-movie-database.html", - "type": "Library", - "license": "MIT", - "authors": [ - { - "name": "Abed Halawi", - "email": "abed.halawi@vinelab.com" - } - ], - "minimum-stability": "dev", - "require": { - "vinelab/neoeloquent": "1.5.*@dev", - "symfony/console": "*" - }, - "autoload": { - "classmap": [ - "models" - ] - } -} diff --git a/Examples/Movies/config/database.php b/Examples/Movies/config/database.php deleted file mode 100644 index 53b338ac..00000000 --- a/Examples/Movies/config/database.php +++ /dev/null @@ -1,28 +0,0 @@ - 'neo4j', - 'host' => 'dev', - 'port' => 7474, - 'username' => 'neo4j', - 'password' => 'neo4j' -]; - -Vinelab\NeoEloquent\Neo4j::connection($connection); - -$capsule = new Capsule; -$manager = $capsule->getDatabaseManager(); -$manager->extend('neo4j', function($config) -{ - $conn = new Connection($config); - $conn->setSchemaGrammar(new CypherGrammar); - return $conn; -}); - -$capsule->addConnection($config); -$capsule->setAsGlobal(); -$capsule->bootEloquent(); diff --git a/Examples/Movies/models/Actor.php b/Examples/Movies/models/Actor.php deleted file mode 100644 index 0a1f74b0..00000000 --- a/Examples/Movies/models/Actor.php +++ /dev/null @@ -1,15 +0,0 @@ -hasMany('Movie', 'ACTS_IN'); - } -} diff --git a/Examples/Movies/models/Movie.php b/Examples/Movies/models/Movie.php deleted file mode 100644 index 8deb261e..00000000 --- a/Examples/Movies/models/Movie.php +++ /dev/null @@ -1,15 +0,0 @@ -belongsToMany('Actor', 'ACTS_IN'); - } -} diff --git a/Examples/Movies/start.php b/Examples/Movies/start.php deleted file mode 100644 index 2e946745..00000000 --- a/Examples/Movies/start.php +++ /dev/null @@ -1,98 +0,0 @@ -actors; - - // show movies - $output = new ConsoleOutput(); - $output->writeln(""); - $output->writeln("Showing the actors for the movie: ".$movie->title.""); - $output->writeln("--------------------------------------------"); - foreach ($actors as $actor) { - $output->writeln("- ".$actor->name); - } - $output->writeln(""); -} - -/** - * Show only the actors whose names end with the given letter. - * - * @param string $letter - */ -function showActorsWithNameEndingWith($letter) -{ - $actors = Actor::where('name', '=~', ".*$letter$")->get(); - - // show actors - $output = new ConsoleOutput(); - $output->writeln("Actors with their names ending with the letter \"$letter\""); - $output->writeln("--------------------------------------------------"); - foreach ($actors as $actor) { - $output->writeln("- ".$actor->name); - } - $output->writeln(""); -} - -function countMovies() -{ - $count = Movie::count(); - - $output = new ConsoleOutput(); - $output->writeln("There are $count movies."); - $output->writeln(""); -} - -function countActors() -{ - $count = Actor::count(); - - $output = new ConsoleOutput(); - $output->writeln("There are $count actors."); - $output->writeln(""); -} - -function showAllMovies() -{ - // fetch all movies - $movies = Movie::get(); - - // show all movies - $output = new ConsoleOutput(); - $output->writeln("Movies:"); - $output->writeln("-------"); - foreach ($movies as $movie) { - $output->writeln("- ".$movie->title); - } - $output->writeln(""); -} - -function showAllActors() -{ - // fetch all actors - $actors = Actor::all(); - - // show all actors - $output = new ConsoleOutput(); - $output->writeln("Actors:"); - $output->writeln("-------"); - foreach ($actors as $actor) { - $output->writeln("- ".$actor->name); - } - $output->writeln(''); -} diff --git a/README.md b/README.md index c5323eb5..282f9e7b 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,35 @@ # NeoEloquent -Neo4j Graph Eloquent Driver for Laravel 5. +Combine the world's most powerful graph database with the best web development framework available. + +_The Laravel ecosystem is massive. This library aims to achieve feature parity with the database drivers provided by default in the framework. Advantages of this include, but are not limited to:_ + +- **Frictionless** migration between database paradigms +- **Extreme performance gains** when working with relational data +- **Increased functionality** (createWith, N-degree relations, Cypher, ...) +- **Easy onboarding** (Only learn Cypher, the graph database query language when you hit the limits of the query builder) +- **Worry free** configuration +- **Optional migrations**. Migrations are only needed for indexes, constraints and moving data around. Neo4J itself is schemaless. +- **Support for complex deployments** If you are using Neo4J aura, a cluster or a single instance, the driver will automatically connect to it. +- **Built-in integration** with laravel packages + +Please refer to the [roadmap](#roadmap) for a list of available features and to the [usage](#usage) section for a list of out-of-the-box features that are available from Laravel. + +> NOTE: you are looking at version 2.0. It is currently in alpha stage and contains drastic changes under the hood. Please refer to the [architecture](#architecture) to gain some more insight on what has changed, and why. ## Quick Reference - [Installation](#installation) - - [Configuration](#configuration) - - [Models](#models) + - [Getting Started](#getting-started) + - [Usage](#usage) - [Relationships](#relationships) - - [Edges](#edges) - - [Migration](#migration) - - [Schema](#schema) - - [Aggregates](#aggregates) + - [Diving Deeper](#diving-deeper) - [Only in Neo](#only-in-neo) - [Things To Avoid](#avoid) + - [Roadmap](#roadmap) + - [Architecture](#architecture) + - [Special thanks](#special-thanks) ## Installation @@ -31,20 +46,17 @@ Or add the package to your `composer.json` and run `composer update`. } ``` -Add the service provider in `app/config/app.php`: +The post install script will automatically add the service provider in `app/config/app.php`: ```php -'Vinelab\NeoEloquent\NeoEloquentServiceProvider', +\Vinelab\NeoEloquent\Database\NeoEloquentServiceProvider::class ``` -The service provider will register all the required classes for this package and will also alias -the `Model` class to `NeoEloquent` so you can simply `extend NeoEloquent` in your models. +## Getting started -## Configuration +### Configuration -### Connection -in `app/config/database.php` or in case of an environment-based configuration `app/config/[env]/database.php` -make `neo4j` your default connection: +If you plan on making Neo4J your main database you can make it your default connection: ```php 'default' => 'neo4j', @@ -56,692 +68,160 @@ Add the connection defaults: 'connections' => [ 'neo4j' => [ 'driver' => 'neo4j', - 'host' => 'localhost', - 'port' => '7474', - 'username' => null, - 'password' => null - ] + 'scheme' => env('DB_SCHEME', 'bolt'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', 7687), + 'database' => env('DB_DATABASE', 'neo4j'), + 'username' => env('DB_USERNAME'), + 'password' => env('DB_PASSWORD') + ], ] ``` -### Migration Setup - -If you're willing to have migrations: +### Defining models -- create the folder `app/database/labels` -- modify `composer.json` and add `app/database/labels` to the `classmap` array -- run `composer dump-autoload` - - -### Documentation - -## Models - -- [Node Labels](#namespaced-models) -- [Soft Deleting](#soft-deleting) +You can always extend from the basic Eloquent Model instead of a NeoEloquent model. The configured connection chooses the correct driver under the hood. But you will lose out of some quality of life methods and functionality, especially when defining relations. ```php -class User extends NeoEloquent {} -``` - -As simple as it is, NeoEloquent will generate the default node label from the class name, -in this case it will be `:User`. Read about [node labels here](http://docs.neo4j.org/chunked/stable/rest-api-node-labels.html) - -### Namespaced Models -When you use namespaces with your models the label will consider the full namespace. - -```php -namespace Vinelab\Cms; - -class Admin extends NeoEloquent { } -``` - -The generated label from that relationship will be `VinelabCmsAdmin`, this is necessary to make sure -that labels do not clash in cases where we introduce another `Admin` instance like -`Vinelab\Blog\Admin` then things gets messy with `:Admin` in the database. - -### Custom Node Labels - -You may specify the label(s) you wish to be used instead of the default generated, they are also -case sensitive so they will be stored as put here. - -```php -class User extends NeoEloquent { - - protected $label = 'User'; // or array('User', 'Fan') - - protected $fillable = ['name', 'email']; -} - -$user = User::create(['name' => 'Some Name', 'email' => 'some@email.com']); -``` - -NeoEloquent has a fallback support for the `$table` variable that will be used if found and there was no `$label` defined on the model. - -```php -class User extends NeoEloquent { - - protected $table = 'User'; - +class Article extends \Vinelab\NeoEloquent\Eloquent\Model { } ``` + +You can now use Laravel as normal. All database functionality can now be used interchangeably with other connections and drivers. -Do not worry about the labels formatting, You may specify them as `array('Label1', 'Label2')` or separate them by a column `:` and prepending them with a `:` is optional. - -### Soft Deleting +## Usage -To enable soft deleting you'll need to `use Vinelab\NeoEloquent\Eloquent\SoftDeletingTrait` -instead of `Illuminate\Database\Eloquent\SoftDeletingTrait` and just like Eloquent you'll need the `$dates` in your models as follows: +For general usage, you can simpy refer to the laravel docs. Things only get a little different when working with [relationships](#relationships). -```php -use Vinelab\NeoEloquent\Eloquent\SoftDeletingTrait; +We have compiled a list of all database-related features in Laravel, so you can refer to their docs from here: -class User extends NeoEloquent { +- [Certain validation rules](https://laravel.com/docs/validation#available-validation-rules) +- [Broadcasting](https://laravel.com/docs/broadcasting) +- [Route model binding](https://laravel.com/docs/routing#route-model-binding) +- [Notifications](https://laravel.com/docs/notifications) +- [Queues](https://laravel.com/docs/queues) +- [Authentication](https://laravel.com/docs/authentication) +- [Authorization](https://laravel.com/docs/authorization) +- [Database](https://laravel.com/docs/database) +- [Query builder](https://laravel.com/docs/queries) +- [Pagination](https://laravel.com/docs/pagination) +- [Migrations](https://laravel.com/docs/migrations) +- [Seeding](https://laravel.com/docs/seeding) +- [Eloquent](https://laravel.com/docs/eloquent) +- All packages building on top of laravel! - use SoftDeletingTrait; +Of course, all other laravel features will continue to work as well. - protected $dates = ['deleted_at']; +## Relationships -} -``` +Relationships work out of the box. Basic methods work with foreign key assumptions to maintain backwards compatibility. This means JOINS in disguise! -## Relationships +All relationships provided by Eloquent have an equivalent in neo4j. They can be accessed by adding `Relationship` after the method name. (eg. `belongsTo` becomes `belongsToRelationship`) - [One-To-One](#one-to-one) - [One-To-Many](#one-to-many) - [Many-To-Many](#many-to-many) -- [Polymorphic](#polymorphic) -Let's go through some examples of relationships between Nodes. +This documentation only explains how it uses Neo4J relations instead of foreign keys. We have placed links to the original relationship documentation where needed. ### One-To-One +Please refer to https://laravel.com/docs/eloquent-relationships#one-to-one for a more in depth explanation of the relationship itself. + ```php class User extends NeoEloquent { public function phone() { - return $this->hasOne('Phone'); + return $this->hasOneRelationship('Phone', 'HAS_PHONE'); } ``` -This represents an `OUTGOING` relationship direction from the `:User` node to a `:Phone`. -##### Saving - -```php -$phone = new Phone(['code' => 961, 'number' => '98765432']) -$relation = $user->phone()->save($phone); -``` - -The Cypher performed by this statement will be as follows: - -``` -MATCH (user:`User`) -WHERE id(user) = 1 -CREATE (user)-[:PHONE]->(phone:`Phone` {code: 961, number: '98765432', created_at: 7543788, updated_at: 7543788}) -RETURN phone; -``` +This represents an `OUTGOING` relationship direction from the `:User` node to a `:Phone`. `(:User) - [:HAS_PHONE] -> (:Phone)` ##### Defining The Inverse Of This Relation ```php -class Phone extends NeoEloquent { +class Phone extends \Vinelab\NeoEloquent\Eloquent\Model { public function user() { - return $this->belongsTo('User'); + return $this->belongsToRelation('User', 'HAS_PHONE'); } } ``` This represents an `INCOMING` relationship direction from -the `:User` node to this `:Phone` node. - -##### Associating Models - -Due to the fact that we do not deal with **foreign keys**, in our case it is much -more than just setting the foreign key attribute on the parent model. In Neo4j (and Graph in general) a relationship is an entity itself that can also have attributes of its own, hence the introduction of -[**Edges**](#Edges) - -> *Note:* Associated models does not persist relations automatically when calling `associate()`. - -```php -$account = Account::find(1986); - -// $relation will be Vinelab\NeoEloquent\Eloquent\Edges\EdgeIn -$relation = $user->account()->associate($account); - -// Save the relation -$relation->save(); -``` - -The Cypher performed by this statement will be as follows: - -``` -MATCH (account:`Account`), (user:`User`) -WHERE id(account) = 1986 AND id(user) = 9862 -MERGE (account)<-[rel_user_account:ACCOUNT]-(user) -RETURN rel_user_account; -``` +the `:User` node to this `:Phone` node. `(:Phone) <- [:HAS_PHONE] - (:USER)` ### One-To-Many +Please refer to https://laravel.com/docs/eloquent-relationships#one-to-many for a more in-depth explanation of the relationship. + ```php -class User extends NeoEloquent { +class User extends \Vinelab\NeoEloquent\Eloquent\Model { public function posts() { - return $this->hasMany('Post', 'POSTED'); + return $this->hasManyRelation('Post', 'POSTED'); } } ``` -This represents an `OUTGOING` relationship direction -from the `:User` node to the `:Post` node. - -```php -$user = User::find(1); -$post = new Post(['title' => 'The Title', 'body' => 'Hot Body']); -$user->posts()->save($post); -``` - -Similar to `One-To-One` relationships the returned value from a `save()` statement is an -`Edge[In|Out]` - -The Cypher performed by this statement will be as follows: +> NOTE: The attentive reader might figure out that there is no difference between the relationships one-to-one and one-to-many in Neo4J. This is because the way foreign-keys are set up in sql. The distinction between one-to-one and one-to-many is purely application based in NeoEloquent. A one-to-one relation boils down to a one-to-many relationship with a result limit of 1. -``` -MATCH (user:`User`) -WHERE id(user) = 1 -CREATE (user)-[rel_user_post:POSTED]->(post:`Post` {title: 'The Title', body: 'Hot Body', created_at: '15-05-2014', updated_at: '15-05-2014'}) -RETURN rel_user_post; -``` +This represents an `OUTGOING` relationship direction +from the `:User` node to the `:Post` node. `(:User) - [:POSTED] -> (:Post)` -##### Defining The Inverse Of This Relation +#### Defining The Inverse Of This Relation ```php -class Post extends NeoEloquent { +class Post extends \Vinelab\NeoEloquent\Eloquent\Model { public function author() { - return $this->belongsTo('User', 'POSTED'); + return $this->belongsToRelation('User', 'POSTED'); } } ``` This represents an `INCOMING` relationship direction from -the `:User` node to this `:Post` node. +the `:User` node to this `:Post` node. `(:Post) <- [:POSTED] - (:User)` ### Many-To-Many -```php -class User extends NeoEloquent { - - public function followers() - { - return $this->belongsToMany('User', 'FOLLOWS'); - } -} -``` - -This represents an `INCOMING` relationship between a `:User` node and another `:User`. - -```php -$jd = User::find(1012); -$mc = User::find(1013); -``` - -`$jd` follows `$mc`: - -```php -$jd->followers()->save($mc); -``` - -Or using the `attach()` method: - -```php -$jd->followers()->attach($mc); -// Or.. -$jd->followers()->attach(1013); // 1013 being the id of $mc ($mc->getKey()) -``` - -The Cypher performed by this statement will be as follows: - -``` -MATCH (user:`User`), (followers:`User`) -WHERE id(user) = 1012 AND id(followers) = 1013 -CREATE (followers)-[:FOLLOWS]->(user) -RETURN rel_follows; -``` - -`$mc` follows `$jd` back: - -```php -$mc->followers()->save($jd); -``` - -The Cypher performed by this statement will be as follows: - -``` -MATCH (user:`User`), (followers:`User`) -WHERE id(user) = 1013 AND id(followers) = 1012 -CREATE (user)-[rel_user_followers:FOLLOWS]->(followers) -RETURN rel_follows; -``` - -get the followers of `$jd` - -```php -$followers = $jd->followers; -``` - -The Cypher performed by this statement will be as follows: - -``` -MATCH (user:`User`), (followers:`User`), (user)-[rel_user_followers:FOLLOWS]-(followers) -WHERE id(user) = 1012 -RETURN rel_follows; -``` - -### Dynamic Properties - -```php -class Phone extends NeoEloquent { - - public function user() - { - return $this->belongsTo('User'); - } - -} - -$phone = Phone::find(1006); -$user = $phone->user; -// or getting an attribute out of the related model -$name = $phone->user->name; -``` - -### Polymorphic - -The concept behind Polymorphic relations is purely relational to the bone but when it comes -to graph we are representing it as a [HyperEdge](http://docs.neo4j.org/chunked/stable/cypher-cookbook-hyperedges.html). - -Hyper edges involves three models, the **parent** model, **hyper** model and **related** model -represented in the following figure: - -![HyperEdges](https://googledrive.com/host/0BznzZ2lBbT0cLW9YcjNldlJkcXc/HyperEdge.png "HyperEdges") - -Similarly in code this will be represented by three models `User` `Comment` and `Post` -where a `User` with id 1 posts a `Post` and a `User` with id 6 `COMMENTED` a `Comment` `ON` that `Post` -as follows: - -```php -class User extends NeoEloquent { - - public function comments($morph = null) - { - return $this->hyperMorph($morph, 'Comment', 'COMMENTED', 'ON'); - } - -} -``` - -In order to keep things simple but still involving the three models we will have to pass the -`$morph` which is any `commentable` model, in our case it's either a `Video` or a `Post` model. - -> **Note:** Make sure to have it defaulting to `null` so that we can Dynamicly or Eager load -with `$user->comments` later on. - -Creating a `Comment` with the `create()` method. - -```php -$user = User::find(6); -$post = Post::find(2); - -$user->comments($post)->create(['text' => 'Totally agree!', 'likes' => 0, 'abuse' => 0]); -``` - -As usual we will have returned an Edge, but this time it's not directed it is an instance of -`HyperEdge`, read more about [HyperEdges here](#hyperedge). - -Or you may save a Comment instance: - -```php -$comment = new Comment(['text' => 'Magnificent', 'likes' => 0, 'abuse' => 0]); - -$user->comments($post)->save($comment); -``` - -Also all the functionalities found in a `BelongsToMany` relationship are supported like -attaching models by Ids: - -```php -$user->comments($post)->attach([$id, $otherId]); -``` - -Or detaching models: - -```php -$user->comments($post)->detach($comment); // or $comment->id -``` - -Sync too: - -```php -$user->comments($post)->sync([$id, $otherId, $someId]); -``` - -#### Retrieving Polymorphic Relations - -From our previous example we will use the `Video` model to retrieve their comments: - -```php -class Video extends NeoEloquent { - - public function comments() - { - return $this->morphMany('Comment', 'ON'); - } - -} -``` - -##### Dynamicly Loading Morph Model - -```php -$video = Video::find(3); -$comments = $video->comments; -``` - -##### Eager Loading Morph Model - -```php -$video = Video::with('comments')->find(3); -foreach ($video->comments as $comment) -{ - // -} -``` - -#### Retrieving The Inverse of a Polymorphic Relation - -```php -class Comment extends NeoEloquent { - - public function commentable() - { - return $this->morphTo(); - } - -} -``` - -```php -$postComment = Comment::find(7); -$post = $comment->commentable; - -$videoComment = Comment::find(5); -$video = $comment->commentable; - -// You can also eager load them -Comment::with('commentable')->get(); -``` - -You may also specify the type of morph you would like returned: +Please refer to https://laravel.com/docs/eloquent-relationships#many-to-many for a more in depth explanation of the relationship. ```php -class Comment extends NeoEloquent { +class User extends \Vinelab\NeoEloquent\Eloquent\Model { - public function post() - { - return $this->morphTo('Post', 'ON'); - } - - public function video() - { - return $this->morphTo('Video', 'ON'); - } - -} -``` - -#### Polymorphic Relations In Short - -To drill things down here's how our three models involved in a Polymorphic relationship connect: - -```php -class User extends NeoEloquent { - - public function comments($morph = null) - { - return $this->hyperMorph($morph, 'Comment', 'COMMENTED', 'ON'); - } - -} -``` - -```php -class Post extends NeoEloquent { // Video is the same as this one - - public function comments() - { - return $this->morphMany('Comment', 'ON'); - } - -} -``` - -```php -class Comment extends NeoEloquent { - - public function commentable() - { - return $this->morphTo(); - } - -} - -``` - -### Eager Loading - -```php -class Book extends NeoEloquent { - - public function author() - { - return $this->belongsTo('Author'); - } -} -``` - -Loading authors with their books with the least performance overhead possible. - -```php -foreach (Book::with('author')->get() as $book) -{ - echo $book->author->name; -} -``` - -Only two Cypher queries will be run in the loop above: - -``` -MATCH (book:`Book`) RETURN *; - -MATCH (book:`Book`), (book)<-[:WROTE]-(author:`Author`) WHERE id(book) IN [1, 2, 3, 4, 5, ...] RETURN book, author; -``` - -## Edges - -- [EdgeIn](#edgein) -- [EdgeOut](#edgeout) -- [HyperEdge](#hyperedge) -- [Working with Edges](#working-with-edges) -- [Edge Attributes](#edge-attributes) - -### Introduction - -Due to the fact that relationships in Graph are much different than other database types so -we will have to handle them accordingly. Relationships have directions that can vary between -**In** and **Out** respectively towards the parent node. - -Edges give you the ability to manipulate relationships properties the same way you do with models. - -```php -$edge = $location->associate($user); -$edge->last_visited = 'today'; -$edge->save(); // true -``` - -#### EdgeIn - -Represents an `INCOMING` direction relationship from the related model towards the parent model. - -```php -class Location extends NeoEloquent { - - public function user() - { - return $this->belongsTo('User', 'LOCATED_AT'); - } - -} -``` - -To associate a `User` to a `Location`: - -```php -$location = Location::find(1922); -$user = User::find(3876); -$relation = $location->associate($user); -``` - -which in Cypher land will map to `(:Location)<-[:LOCATED_AT]-(:User)` and `$relation` -being an instance of `EdgeIn` representing an incoming relationship towards the parent. - -And you can still access the models from the edge: - -```php -$relation = $location->associate($user); -$location = $relation->parent(); -$user = $relation->related(); -``` - -#### EdgeOut - -Represents an `OUTGOING` direction relationship from the parent model to the related model. - -```php -class User extends NeoEloquent { - - public function posts() + public function followers() { - return $this->hasMany('Post', 'POSTED'); + return $this->belongsToManyRelation('User', 'FOLLOWS>'); } - } ``` +This represents an `Outgoing` relationship between a `:User` node and another `:User`. `(:User) - [:FOLLOWS] -> (:User)` -To save an outgoing edge from `:User` to `:Post` it goes like: +Belongs to many uses a relationship as a table. In other words, the pivot table is a relationship in Neo4J. When you define properties on the pivot table, you define them on the relationship. -```php -$post = new Post(['...']); -$posted = $user->posts()->save($post); -``` +Since a relationship must always have a direction when creating it, you need to annotate the direction with an arrow like ``. -Which in Cypher would be `(:User)-[:POSTED]->(:Post)` and `$posted` being the `EdgeOut` instance. +### Polymorphic relationships -And fetch the related models: - -```php -$edge = $user->posts()->save($post); -$user = $edge->parent(); -$post = $edge->related(); -``` - -#### HyperEdge - -This edge comes as a result of a [Polymorphic Relation](#polymorphic) representing an edge involving -two other edges **left** and **right** that can be accessed through the `left()` and `right()` methods. - -This edge is treated a bit different than the others since it is not a direct relationship -between two models which means it has no specific direction. - -```php -$edge = $user->comments($post)->attach($comment); -// Access the left and right edges -$left = $edge->left(); -$user = $left->parent(); -$comment = $left->related(); - -$right = $edge->right(); -$comment = $right->parent(); -$post = $right->related(); -``` +Polymorphic relationships are completely superfluous in Neo4J. A relationship does not care about the label of the start or end node. Because of this, all morphing relationships can be reduced to their normal equivalent. -### Working With Edges +You can refer to the morphing relationships [here](https://laravel.com/docs/eloquent-relationships#polymorphic-relationships) and convert them to their non-morphing relationship equivalent based on the table below: -As stated earlier **Edges** are entities to Graph unlike *SQL* where they are a matter of a -foreign key having the value of the parent model as an attribute on the belonging model or in -*Documents* where they are either embeds or ids as references. So we developed them to be *light -models* which means you can work with them as if you were working with an `Eloquent` instance - to a certain extent, -except [HyperEdges](#hyperedges). +| Morphing relationship | NeoEloquent equivalent | +|-----------------------|------------------------| +| morphTo | belongsToRelation | +| morphOne | hasOneRelation | +| morphTo | belongsToRelation | +| morphMany | hasManyRelation | +| morphToMany | belongsToManyRelation | +| morphedByMany | belongsToManyRelation | -```php -// Create a new relationship -$relation = $location->associate($user); // Vinelab\NeoEloquent\Eloquent\Edges\EdgeIn - -// Save the relationship to the database -$relation->save(); // true -``` - -In the case of a `HyperEdge` you can access all three models as follows: - -```php -$edge = $user->comments($post)->save($comment); -$user = $edge->parent(); -$comment = $edge->hyper(); -$post = $edge->related(); -``` - -#### Edge Attributes - -By default, edges will have the timestamps `created_at` and `updated_at` automatically set and updated **only if** timestamps are enabled by setting `$timestamps` to `true` -on the parent model. - -```php -$located_at = $location->associate($user); -$located_at->since = 1966; -$located_at->present = true; -$located_at->save(); - -// $created_at and $updated_at are Carbon\Carbon instances -$created_at = $located_at->created_at; -$updated_at = $located_at->updated_at; -``` - -##### Retrieve an Edge from a Relation - -The same way an association will create an `EdgeIn` relationship we can retrieve -the edge between two models by calling the `edge($model)` method on the `belongsTo` -relationship. - -```php -$location = Location::find(1892); -$edge = $location->user()->edge(); -``` - -You may also specify the model at the other side of the edge. - -> Note: By default NeoEloquent will try to pefrorm the `$location->user` internally to figure -out the related side of the edge based on the relation function name, in this case it's -`user()`. - -```php -$location = Location::find(1892); -$edge = $location->user()->edge($location->user); -``` ## Only in Neo @@ -879,223 +359,164 @@ WHERE id(tag) IN [1, 2] CREATE (post)-[:TAG]->(tag); ``` +## Avoid -## Migration -For migrations to work please perform the following: - -- create the folder `app/database/labels` -- modify `composer.json` and add `app/database/labels` to the `classmap` array - -Since Neo4j is a schema-less database you don't need to predefine types of properties for labels. -However you will be able to perform [Indexing](http://neo4j.com/docs/stable/query-schema-index.html) and [Constraints](http://neo4j.com/docs/stable/query-constraints.html) using NeoEloquent's pain-less [Schema](#schema). - -#### Commands -NeoEloquent introduces new commands under the `neo4j` namespace so you can still use Eloquent's migration commands side-by-side. - -Migration commands are the same as those of Eloquent, in the form of `neo4j:migrate[:command]` - - neo4j:make:migration Create a new migration file - neo4j:migrate Run the database migrations - neo4j:migrate:reset Rollback all database migrations - neo4j:migrate:refresh Reset and re-run all migrations - neo4j:migrate:rollback Rollback the last database migration - - -### Creating Migrations - -Like in Laravel you can create a new migration by using the `make` command with Artisan: - - php artisan neo4j:migrate:make create_user_label - -Label migrations will be placed in `app/database/labels` - -You can add additional options to commands like: - - php artisan neo4j:migrate:make foo --path=app/labels - php artisan neo4j:migrate:make create_user_label --create=User - php artisan neo4j:migrate:make create_user_label --label=User - - -### Running Migrations - -##### Run All Outstanding Migrations - - php artisan neo4j:migrate - -##### Run All Outstanding Migrations For A Path - - php artisan neo4j:migrate --path=app/foo/labels - -##### Run All Outstanding Migrations For A Package - - php artisan neo4j:migrate --package=vendor/package - ->Note: If you receive a "class not found" error when running migrations, try running the `composer dump-autoload` command. - -#### Forcing Migrations In Production - -To force-run migrations on a production database you can use: - - php artisan neo4j:migrate --force +Beware of these common pitfalls. -### Rolling Back Migrations +### JOINS :confounded: -##### Rollback The Last Migration Operation +_You were so preoccupied with whether you could, you did not stop to consider if you should._ - php artisan neo4j:migrate:rollback +Joins make no sense for Graph, we have relationships! -##### Rollback all migrations +They are available to achieve feature parity, but Neo4J will issue warnings if you do use them. Please refer to the [relationship](#relationships) section find better ways for defining relations. - php artisan neo4j:migrate:reset +### Eloquent relationships -##### Rollback all migrations and run them all again +If you are using the same methods found in the laravel documentation for defining relationships between models, you will be using the foreign-key assumptions, which are joins in disguise! - php artisan neo4j:migrate:refresh +All model relationship have a neo4j relationship equivalent, using neo4j relationships instead of joins. Please refer to [Relationships](#relationships) for more information. - php artisan neo4j:migrate:refresh --seed +### Nested Arrays -## Schema -NeoEloquent will alias the `Neo4jSchema` facade automatically for you to be used in manipulating labels. +Nested arrays are not supported in Neo4J. If you ever find yourself creating them, you are probably confronting an anti-pattern: ```php -Neo4jSchema::label('User', function(Blueprint $label) -{ - $label->unique('uuid'); -}); +User::create(['name' => 'Some Name', 'location' => ['lat' => 123, 'lng'=> -123 ] ]); ``` -If you decide to write Migration classes manually (not using the generator) make sure to have these `use` statements in place: - -- `use Vinelab\NeoEloquent\Schema\Blueprint;` -- `use Vinelab\NeoEloquent\Migrations\Migration;` - -Currently Neo4j supports `UNIQUE` constraint and `INDEX` on properties. You can read more about them at - - - -#### Schema Methods - -Command | Description ------------- | ------------- -`$label->unique('email')` | Adding a unique constraint on a property -`$label->dropUnique('email')` | Dropping a unique constraint from property -`$label->index('uuid')` | Adding index on property -`$label->dropIndex('uuid')` | Dropping index from property - -### Droping Labels - -```php -Neo4jSchema::drop('User'); -Neo4jSchema::dropIfExists('User'); -``` +Check out the [createWith()](#createwith) method on how you can achieve this in a Graph way. The nested attributes should be encapsulated in another node. -### Renaming Labels +## Diving deeper -```php -Neo4jSchema::renameLabel($from, $to); -``` +### Juggling connections -### Checking Label's Existence +If you are juggling multiple connections/databases you can always change the connections for any database related classes manually. Examples include, but are not limited to: Models, Query builders, Schema, Basic queries, etc. +_For Models_ ```php -if (Neo4jSchema::hasLabel('User')) { - -} else { - +class Neo4JArticle extends \Vinelab\NeoEloquent\Eloquent\Model { + protected $connection = 'neo4j'; } -``` - -### Checking Relation's Existence - -```php -if (Neo4jSchema::hasRelation('FRIEND_OF')) { - -} else { +class SqlArticle extends \Illuminate\Database\Eloquent\Model { + protected $connection = 'mysql'; } ``` -You can read more about migrations and schema on: +_For Query Builders and direct queries_ - - - +```php +use Illuminate\Support\Facades\DB; -## Aggregates +$neo4jArticle = DB::connection('neo4j') + ->table('Article') + ->where('x', 'y') + ->first(); -In addition to the Eloquent builder aggregates, NeoEloquent also has support for -Neo4j specific aggregates like *percentile* and *standard deviation*, keeping the same -function names for convenience. -Check [the docs](http://docs.neo4j.org/chunked/stable/query-aggregation.html) for more. +$sqlArticle = DB::connection('mysql') + ->table('articles') + ->where('x', 'y') + ->first(); -> `table()` represents the label of the model +DB::connection('neo4j')->insert(<<<'CYPHER' +CREATE (a:Article {title: $title}) +CYPHER, ['title' => 'My awesome blog post']); +DB::connection('mysql')->insert(<<<'SQL' +INSERT INTO articles (title) +VALUES (?); +SQL, ['My awesome blog post']); ``` -$users = DB::table('User')->count(); - -$distinct = DB::table('User')->countDistinct('points'); -$price = DB::table('Order')->max('price'); - -$price = DB::table('Order')->min('price'); - -$price = DB::table('Order')->avg('price'); +_For Schema Builders / Migrations (Work in progress)_ +```php +use Illuminate\Support\Facades\Schema; +use Illuminate\Database\Schema\Blueprint; -$total = DB::table('User')->sum('votes'); +Schema::connection('neo4j')->create('Article', function (Blueprint $node) { + $node->increments('id'); + $node->index('createdAt'); + $node->index('updatedAt'); + $node->index('title'); +}); -$disc = DB::table('User')->percentileDisc('votes', 0.2); +Schema::connection('neo4j')->create('Article', function (Blueprint $node) { + $node->increments('id'); + $node->index('createdAt'); + $node->index('updatedAt'); + $node->index('title'); +}); +``` -$cont = DB::table('User')->percentileCont('votes', 0.8); +### Tables, nodes and labels -$deviation = DB::table('User')->stdev('sex'); +#### Why we use tables instead of nodes and labels -$population = DB::table('User')->stdevp('sex'); +In our never-ending quest of achieving feature-parity, we landed on the design decision to keep the word table in the initial stage of the library. This might be strange if you are a fellow graph-aficionado. Laravel is built with relational databases in mind, which only knows tables, while Neo4J only knows nodes and relationships. NeoEloquent treats relationship types and node labels as the equivalent of a table. -$emails = DB::table('User')->collect('email'); -``` +If you are creating a new query or model, you will have to use the table word while in reality you are either defining a label or relationship type, depending on the context. Please refer to [architecture](#architecture) for a more in-depth explanation. -## Changelog -Check the [Releases](https://github.com/Vinelab/NeoEloquent/releases) for details. +The previous version used the word label, but that makes for some confusing instances in the rare case the label is actually a relationship type or when the end user is not aware of the label keyword and makes futile attempts when defining a table. -## Avoid +Please join the label discussion [here](), which is scheduled for release 2.1. -Here are some constraints and Graph-specific gotchas, a list of features that are either not supported or not recommended. +#### Implicit table naming -### JOINS :confounded: +If you are using NeoEloquent and have not explicitly defined a table, the table name will be guessed based on the class name. The table will be the studly-case of the class basename `$this->table ?? Str::studly(class_basename($this))`. -- They make no sense for Graph, plus Graph hates them! -Which makes them unsupported on purpose. If migrating from an `SQL`-based app -they will be your boogie monster. +## Roadmap -### Pivot Tables in Many-To-Many Relationships -This is not supported, instead we will be using [Edges](#edges) to work with relationships between models. +This version is currently in alpha. In order for it to be released there are a few more fixes that need to happen. The overview can be found here: -### Nested Arrays and Objects +| Feature | Completed? | +|--------------------------------|--------------------------------| +| Automatic connection resolving | yes | +| Transactions | yes | +| Connection statement handling | yes | +| Selects | yes | +| Columns | yes | +| Wheres | almost all | +| Nested wheres | yes | +| Exists | yes | +| Insert | all except pivot relationships | +| Update | yes | +| Delete | yes | +| Union | yes | +| Join | yes | +| Limit | yes | +| Offset | yes | +| Orders | yes | +| Having | testing | +| Groups | testing | +| Truncate | yes | +| Aggregate | yes | +| One-to-one relationships | yes | +| One-to-many relationships | yes | +| Many-to-many relationships | work in progress | +| Relationship preloading | no | +| Schema | no | +| createWith | out-of-order | +| label variables and methods | under discussion | +| multiple labels | under discussion | +| N-degree relationships | under discussion | -- Due to the limitations imposed by the objects map types that can be stored in a single, -you can never have nested *arrays* or *objects* in a single model, -make sure it's flat. *Example:* +## Architecture -```php -// Don't -User::create(['name' => 'Some Name', 'location' => ['lat' => 123, 'lng'=> -123 ] ]); -``` +There are two main classes doing the heavy lifting: -Check out the [createWith()](#createwith) method on how you can achieve this in a Graph way. +1. The `Connection` class, which delegates the queries and parameters to the underlying Neo4J driver. +2. The `DSLGrammar` class, which converts the Query Builder to their respective Cypher DSL. The `CypherGrammar` class then converts the DSL to cypher strings. -## Tests +These two classes offer the deepest possible level of integration within the Laravel Framework. Other classes such as the relations, query and eloquent builder simply offer specific methods or constructors to help mitigate the few inconsistencies between SQL and Cypher that are impossible to solve otherwise. -- install a Neo4j instance and run it with the default configuration `localhost:7474` -- make sure the database graph is empty to avoid conflicts -- after running `composer install` there should be `/vendor/bin/phpunit` -- run `./vendor/bin/phpunit` after making sure that the Neo4j instance is running +## Special Thanks -> Tests marked as incomplete means they are either known issues or non-supported features, -check included messages for more info. +This package is a huge undertaking built on top of the thriving Neo4J PHP ecosystem. Special thanks are in order: -## Factories - - > You can use default Laravel `factory()` helper for NeoEloquent models too. +- [Michal Štefaňák](https://github.com/stefanak-michal), maintainer of the [bolt library](https://github.com/neo4j-php/Bolt), without whom it wouldn't even be possible to connect to Neo4J in the first place +- [Marijn van Wezel](https://github.com/marijnvanwezel), maintainer of the [PHP cypher DSL](https://github.com/WikibaseSolutions/php-cypher-dsl), whose library provided useful abstractions making it possible to convert the SQL assumptions of Laravel to Cypher queries. +- [Abed Halawi](https://github.com/Mulkave), maintainer and pioneer of the NeoEloquent library +- [Ghlen Nagels](https://github.com/transistive), maintainer of the [driver and client](https://github.com/neo4j-php/neo4j-php-client) +- [Neo4J](https://neo4j.com) for providing the resources and fertile soil to allow the community to grow. In particular to [Florent](https://github.com/fbiville) and [Michael](https://twitter.com/mesirii) - - define needed factories inside `database/factories/`(read more)[https://laravel.com/docs/5.6/database-testing#writing-factories]; - - use `factory()` in the same style as default Laravel `factory()`. diff --git a/composer.json b/composer.json index 846a36c1..59a782c2 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "vinelab/neoeloquent", - "description": "Laravel wrapper for the Neo4j graph database REST interface", + "description": "Neo4j database driver for Laravel integrating with the query builder and Laravel models.", "keywords": [ "neo4j", "ogm", @@ -17,27 +17,28 @@ { "name": "Kinane Domloje", "email": "kinane@vinelab.com" + }, + { + "name": "Ghlen Nagels", + "email": "ghlen@pm.me" } ], "require": { - "php": ">=7.4", - "illuminate/container": "^8.0", - "illuminate/contracts": "^8.0", - "illuminate/database": "^8.0", - "illuminate/events": "^8.0", - "illuminate/support": "^8.0", - "illuminate/pagination": "^8.0", - "nesbot/carbon": "^2.0", - "laudis/neo4j-php-client": "^2.3.3" + "php": "^8.1", + "laudis/neo4j-php-client": "^3.0.1", + "psr/container": "^1.1.2", + "illuminate/contracts": "^10.0", + "illuminate/database": "^10.0", + "php-graph-group/cypher-query-builder": "dev-master" }, "require-dev": { - "mockery/mockery": "~1.3.0", - "phpunit/phpunit": "^9.0", - "symfony/var-dumper": "*", - "fzaninotto/faker": "~1.4", - "composer/composer": "^2.1" + "phpunit/phpunit": "^10.0.19", + "laravel/pint": "^1.8", + "vimeo/psalm": "^5.9", + "psalm/plugin-laravel": "^2.8" }, "autoload": { + "psr-4": { "Vinelab\\NeoEloquent\\": "src/" } @@ -54,5 +55,10 @@ "Vinelab\\NeoEloquent\\NeoEloquentServiceProvider" ] } + }, + "config": { + "allow-plugins": { + "php-http/discovery": true + } } } diff --git a/docker-compose.yml b/docker-compose.yml index 4c18cd33..16cdcd1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,24 +1,32 @@ -# Compose File Reference: https://docs.docker.com/compose/compose-file/ version: '3.7' +networks: + neo-eloquent: services: - # Docker Image: https://hub.docker.com/r/vinelab/nginx-php app: build: context: . ports: - ${DOCKER_HOST_APP_PORT:-8000}:80 volumes: - - ./:/code:cached + - ./:/opt/project environment: - - XDEBUG_CONFIG=remote_host=host.docker.internal + - NEO4J_HOST=neo4j + - NEO4J_DATABASE=neo4j + - NEO4J_PORT=7687 + - NEO4J_USER=neo4j + - NEO4J_PASSWORD=testtest + working_dir: /opt/project + networks: + - neo-eloquent - # Docker Image: https://hub.docker.com/_/neo4j neo4j: environment: - - NEO4J_AUTH=none - image: neo4j:4.0 + - NEO4J_AUTH=neo4j/testtest + image: neo4j:5 ports: - ${DOCKER_HOST_NEO4J_HTTP_PORT:-7474}:7474 - ${DOCKER_HOST_NEO4J_BOLT_PORT:-7687}:7687 + networks: + - neo-eloquent diff --git a/phpunit.xml b/phpunit.xml index ea26d374..9f08602b 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,24 +1,11 @@ - - - - ./tests/Vinelab - ./tests/functional - - - ./tests/functional - - - ./tests/Vinelab - - + + + + ./tests/Functional + + + + + diff --git a/phpunit.xml.bak b/phpunit.xml.bak new file mode 100644 index 00000000..62430785 --- /dev/null +++ b/phpunit.xml.bak @@ -0,0 +1,27 @@ + + + + + ./tests/Vinelab + ./tests/functional + + + ./tests/functional + + + ./tests/Vinelab + + + + + + diff --git a/pint.json b/pint.json new file mode 100644 index 00000000..70b0e18b --- /dev/null +++ b/pint.json @@ -0,0 +1,3 @@ +{ + "preset": "laravel" +} \ No newline at end of file diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 00000000..8e033104 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Capsule/Manager.php b/src/Capsule/Manager.php deleted file mode 100644 index 32a7e1e1..00000000 --- a/src/Capsule/Manager.php +++ /dev/null @@ -1,196 +0,0 @@ -setupContainer($container ?: new Container()); - - // Once we have the container setup, we will setup the default configuration - // options in the container "config" binding. This will make the database - // manager behave correctly since all the correct binding are in place. - $this->setupDefaultConfiguration(); - - $this->setupManager(); - } - - /** - * Setup the default database configuration options. - */ - protected function setupDefaultConfiguration() - { - $this->container['config']['database.default'] = 'neo4j'; - } - - /** - * Build the database manager instance. - */ - protected function setupManager() - { - $factory = new ConnectionFactory($this->container); - - $this->manager = new DatabaseManager($this->container, $factory); - } - - /** - * Get a connection instance from the global manager. - * - * @param string $connection - * - * @return \Illuminate\Database\Connection - */ - public static function connection($connection = null) - { - return static::$instance->getConnection($connection); - } - - /** - * Get a fluent query builder instance. - * - * @param string $table - * @param string $connection - * - * @return \Illuminate\Database\Query\Builder - */ - public static function table($table, $connection = null) - { - return static::$instance->connection($connection)->table($table); - } - - /** - * Get a schema builder instance. - * - * @param string $connection - * - * @return \Illuminate\Database\Schema\Builder - */ - public static function schema($connection = null) - { - return static::$instance->connection($connection)->getSchemaBuilder(); - } - - /** - * Get a registered connection instance. - * - * @param string $name - * - * @return \Illuminate\Database\Connection - */ - public function getConnection($name = null) - { - return $this->manager->connection($name); - } - - /** - * Register a connection with the manager. - * - * @param array $config - * @param string $name - */ - public function addConnection(array $config, $name = 'default') - { - $connections = $this->container['config']['database.connections']; - - $connections[$name] = $config; - - $this->container['config']['database.connections'] = $connections; - } - - /** - * Bootstrap Eloquent so it is ready for usage. - */ - public function bootEloquent() - { - Eloquent::setConnectionResolver($this->manager); - - // If we have an event dispatcher instance, we will go ahead and register it - // with the Eloquent ORM, allowing for model callbacks while creating and - // updating "model" instances; however, if it not necessary to operate. - if ($dispatcher = $this->getEventDispatcher()) { - Eloquent::setEventDispatcher($dispatcher); - } - } - - /** - * Set the fetch mode for the database connections. - * - * @param int $fetchMode - * - * @return $this - */ - public function setFetchMode($fetchMode) - { - $this->container['config']['database.fetch'] = $fetchMode; - - return $this; - } - - /** - * Get the database manager instance. - * - * @return \Illuminate\Database\DatabaseManager - */ - public function getDatabaseManager() - { - return $this->manager; - } - - /** - * Get the current event dispatcher instance. - * - * @return \Illuminate\Contracts\Events\Dispatcher|null - */ - public function getEventDispatcher() - { - if ($this->container->bound('events')) { - return $this->container['events']; - } - } - - /** - * Set the event dispatcher instance to be used by connections. - * - * @param \Illuminate\Contracts\Events\Dispatcher $dispatcher - */ - public function setEventDispatcher(Dispatcher $dispatcher) - { - $this->container->instance('events', $dispatcher); - } - - /** - * Dynamically pass methods to the default connection. - * - * @param string $method - * @param array $parameters - * - * @return mixed - */ - public static function __callStatic($method, $parameters) - { - return call_user_func_array([static::connection(), $method], $parameters); - } -} diff --git a/src/Concerns/AsRelationship.php b/src/Concerns/AsRelationship.php new file mode 100644 index 00000000..ca74a03e --- /dev/null +++ b/src/Concerns/AsRelationship.php @@ -0,0 +1,327 @@ +timestamps = $instance->hasTimestampAttributes($attributes); + + $instance->setTable($tableWithRelationshipEncoded) + ->forceFill($attributes) + ->syncOriginal(); + + $instance->leftKeyPropertyName = $leftKeyPropertyName; + $instance->leftLabel = $leftLabel; + $instance->rightKeyPropertyName = $rightKeyPropertyName; + $instance->rightLabel = $rightLabel; + + $instance->exists = $exists; + + return $instance; + } + + /** + * Create a new relationship model from raw values returned from a query. + */ + public static function fromRawAttributes( + array $attributes, + string $tableWithRelationshipEncoded, + string $leftKeyPropertyName, + string $leftLabel, + string $rightKeyPropertyName, + string $rightLabel, + bool $exists = false + ): static { + $instance = static::fromAttributes( + [], + $tableWithRelationshipEncoded, + $leftKeyPropertyName, + $leftLabel, + $rightKeyPropertyName, + $rightLabel, + $exists + ); + + $instance->timestamps = $instance->hasTimestampAttributes($attributes); + + $instance->setRawAttributes(array_merge($instance->getRawOriginal(), $attributes), $exists); + + return $instance; + } + + public function getLeftKeyValue(): mixed + { + return $this->leftKeyValue; + } + + public function getLeftKeyPropertyName(): string + { + return $this->leftKeyPropertyName; + } + + public function setLeftKeyPropertyName(string $leftKeyPropertyName): void + { + $this->leftKeyPropertyName = $leftKeyPropertyName; + } + + public function getLeftLabel(): string + { + return $this->leftLabel; + } + + public function setLeftLabel(string $leftLabel): void + { + $this->leftLabel = $leftLabel; + } + + public function getRightKeyPropertyName(): string + { + return $this->rightKeyPropertyName; + } + + public function setRightKeyPropertyName(string $rightKeyPropertyName): void + { + $this->rightKeyPropertyName = $rightKeyPropertyName; + } + + public function getRightLabel(): string + { + return $this->rightLabel; + } + + public function setRightLabel(string $rightLabel): void + { + $this->rightLabel = $rightLabel; + } + + public function getRightKeyValue(): mixed + { + return $this->rightKeyValue; + } + + public function setRightKeyValue(mixed $rightKeyValue): void + { + $this->rightKeyValue = $rightKeyValue; + } + + public function setLeftKeyValue(mixed $leftKeyValue): void + { + $this->leftKeyValue = $leftKeyValue; + } + + /** + * Set the keys for a select query. + * + * @param EloquentBuilder $query + */ + protected function setKeysForSelectQuery($query): EloquentBuilder + { + if (isset($this->attributes[$this->getKeyName()])) { + return parent::setKeysForSelectQuery($query); + } + + $query->where($this->rightKeyPropertyName, $this->getOriginal( + $this->foreignKey, $this->getAttribute($this->foreignKey) + )); + + return $query->where($this->leftKeyPropertyName, $this->getOriginal( + $this->leftKeyPropertyName, $this->getAttribute($this->leftKeyPropertyName) + )); + } + + /** + * Set the keys for a save update query. + * + * @param EloquentBuilder $query + */ + protected function setKeysForSaveQuery($query): EloquentBuilder + { + return $this->setKeysForSelectQuery($query); + } + + /** + * Delete the relationship model record from the database. + */ + public function delete(): int + { + if (isset($this->attributes[$this->getKeyName()])) { + return (int) parent::delete(); + } + + if ($this->fireModelEvent('deleting') === false) { + return 0; + } + + $this->touchOwners(); + + return tap($this->getDeleteQuery()->delete(), function () { + $this->exists = false; + + $this->fireModelEvent('deleted', false); + }); + } + + /** + * Get the query builder for a delete operation on the relationship. + * + * @return EloquentBuilder + */ + protected function getDeleteQuery() + { + return $this->newQueryWithoutRelationships()->where([ + $this->foreignKey => $this->getOriginal($this->foreignKey, $this->getAttribute($this->foreignKey)), + $this->leftKeyPropertyName => $this->getOriginal($this->leftKeyPropertyName, $this->getAttribute($this->leftKeyPropertyName)), + ]); + } + + /** + * Get the table associated with the model. + */ + public function getTable(): string + { + if (! isset($this->table)) { + $table = '<'.Str::upper(Str::snake(Str::singular(class_basename($this)))).'>'; + + $this->setTable($table); + } + + return $this->table; + } + + /** + * @return $this + */ + public function setRelationData( + string $leftKeyPropertyName, + string $leftLabel, + string $rightKeyPropertyName, + string $rightLabel + ): static { + $this->leftKeyPropertyName = $leftKeyPropertyName; + $this->leftLabel = $leftLabel; + $this->rightKeyPropertyName = $rightKeyPropertyName; + $this->rightLabel = $rightLabel; + + return $this; + } + + /** + * Determine if the pivot model or given attributes has timestamp attributes. + * + * @param array|null $attributes + */ + public function hasTimestampAttributes(array|null $attributes = null): bool + { + return array_key_exists($this->getCreatedAtColumn(), $attributes ?? $this->attributes); + } + + /** + * Get the queueable identity for the entity. + */ + public function getQueueableId(): mixed + { + if (isset($this->attributes[$this->getKeyName()])) { + return $this->getKey(); + } + + return sprintf( + '%s:%s:%s:%s', + $this->foreignKey, $this->getAttribute($this->foreignKey), + $this->leftKeyPropertyName, $this->getAttribute($this->leftKeyPropertyName) + ); + } + + /** + * Get a new query to restore one or more models by their queueable IDs. + * + * @param int[]|string[]|string $ids + */ + public function newQueryForRestoration($ids): EloquentBuilder + { + if (is_array($ids)) { + return $this->newQueryForCollectionRestoration($ids); + } + + if (! str_contains($ids, ':')) { + return parent::newQueryForRestoration($ids); + } + + [$leftLabel, $leftKeyName, $leftKeyValue, $rightLabel, $rightKeyName, $rightKeyValue] = explode(':', $ids); + + return $this->newQueryWithoutScopes() + ->leftJoin($leftLabel) + ->whereRightNode($rightLabel) + ->where($segments[0], $segments[1]) + ->where($segments[2], $segments[3]); + } + + /** + * Get a new query to restore multiple models by their queueable IDs. + * + * @param int[]|string[] $ids + */ + protected function newQueryForCollectionRestoration(array $ids): EloquentBuilder + { + $ids = array_values($ids); + + if (! str_contains($ids[0], ':')) { + return parent::newQueryForRestoration($ids); + } + + $query = $this->newQueryWithoutScopes(); + + foreach ($ids as $id) { + $segments = explode(':', $id); + + $query->orWhere(function ($query) use ($segments) { + return $query->where($segments[0], $segments[1]) + ->where($segments[2], $segments[3]); + }); + } + + return $query; + } + + protected function getEncodedData(): string + { + return $this->getTable(); + } + + protected function setEncodedData(string $data): void + { + $this->setTable($data); + } +} diff --git a/src/Concerns/IsGraphAware.php b/src/Concerns/IsGraphAware.php new file mode 100644 index 00000000..9495c03d --- /dev/null +++ b/src/Concerns/IsGraphAware.php @@ -0,0 +1,103 @@ +generateId && + ! array_key_exists('id', $model->attributesToArray()) + ) { + $model->setAttribute('id', Uuid::uuid4()->toString()); + } + }); + } + + public function setLabel(string $label): self + { + return $this->setTable($label); + } + + public function getLabel(): string + { + return $this->getTable(); + } + + public function getForeignKey(): string + { + return Str::studly(class_basename($this)).$this->getKeyName(); + } + + /** + * Get the joining table name for a many-to-many relation. + * + * @param string $related + * @param Model|null $instance + */ + public function joiningTable($related, $instance = null): string + { + // The joining table name, by convention, is simply the snake cased models + // sorted alphabetically and concatenated with an underscore, so we can + // just sort the models and join them together to get the table name. + $segments = [ + $instance ? $instance->joiningTableSegment() : Str::studly(class_basename($related)), + $this->joiningTableSegment(), + ]; + + // Now that we have the model names in an array we can just sort them and + // use the implode function to join them together with an underscores, + // which is typically used by convention within the database system. + sort($segments); + + return strtolower(implode('_', $segments)); + } + + /** + * Get this model's half of the intermediate table name for belongsToMany relationships. + */ + public function joiningTableSegment(): string + { + return Str::studly(class_basename($this)); + } + + /** + * Get the polymorphic relationship columns. + * + * @param string $name + * @param string $type + * @param string $id + */ + protected function getMorphs($name, $type, $id): array + { + return [$type ?: $name.'Type', $id ?: $name.'Id']; + } + + public function getTable(): string + { + return $this->table ?? Str::studly(class_basename($this)); + } +} diff --git a/src/Connection.php b/src/Connection.php index a27dd704..8361011c 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -2,1228 +2,403 @@ namespace Vinelab\NeoEloquent; +use BadMethodCallException; use Closure; -use DateTime; -use Exception; -use Laudis\Neo4j\Authentication\Authenticate; -use Laudis\Neo4j\ClientBuilder; -use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Contracts\ClientInterface; +use Doctrine\DBAL\Types\Type; +use Generator; +use Illuminate\Database\Events\StatementPrepared; +use Illuminate\Database\LostConnectionException; +use Illuminate\Database\Query\Processors\Processor; +use Illuminate\Database\QueryException; +use Illuminate\Support\Arr; +use Laudis\Neo4j\Contracts\SessionInterface; use Laudis\Neo4j\Contracts\TransactionInterface; -use Laudis\Neo4j\Databags\ResultSummary; -use Laudis\Neo4j\Databags\SummarizedResult; -use Laudis\Neo4j\Formatter\OGMFormatter; -use Laudis\Neo4j\Formatter\SummarizedResultFormatter; -use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Databags\SummaryCounters; +use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Types\CypherMap; use LogicException; -use Neoxygen\NeoClient\Client; +use RuntimeException; use Throwable; -use Vinelab\NeoEloquent\Exceptions\InvalidCypherException; -use Vinelab\NeoEloquent\Exceptions\QueryException; -use Vinelab\NeoEloquent\Query\Builder as QueryBuilder; -use Vinelab\NeoEloquent\Query\Expression; -use Vinelab\NeoEloquent\Query\Grammars\CypherGrammar; +use Vinelab\NeoEloquent\Grammars\CypherGrammar; use Vinelab\NeoEloquent\Schema\Builder; use Vinelab\NeoEloquent\Schema\Grammars\CypherGrammar as SchemaGrammar; -use Vinelab\NeoEloquent\Query\Grammars\Grammar; -use Vinelab\NeoEloquent\Query\Processors\Processor; - -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Support\Arr; -use function sprintf; -class Connection implements ConnectionInterface +/** + * @psalm-suppress PropertyNotSetInConstructor + */ +final class Connection extends \Illuminate\Database\Connection { - const TYPE_HA = 'ha'; - const TYPE_MULTI = 'multi'; - const TYPE_SINGLE = 'single'; - - /** - * The reconnector instance for the connection. - * - * @var callable - */ - protected $reconnector; - - /** - * The query grammar implementation. - * - * @var \Illuminate\Database\Query\Grammars\Grammar - */ - protected $queryGrammar; - - /** - * The schema grammar implementation. - * - * @var \Illuminate\Database\Schema\Grammars\Grammar - */ - protected $schemaGrammar; - - /** - * The query post processor implementation. - * - * @var \Illuminate\Database\Query\Processors\Processor - */ - protected $postProcessor; - - /** - * The event dispatcher instance. - * - * @var Dispatcher - */ - protected $events; - - /** - * The number of active transactions. - * - * @var int - */ - protected $transactions = 0; - - /** - * All of the queries run against the connection. - * - * @var array - */ - protected $queryLog = []; - - /** - * Indicates whether queries are being logged. - * - * @var bool - */ - protected $loggingQueries = false; - - /** - * Indicates if the connection is in a "dry run". - * - * @var bool - */ - protected $pretending = false; - - /** - * The name of the connected database. - * - * @var string - */ - protected $database; - - /** - * The database connection configuration options. - * - * @var array - */ - protected $config = []; - - /** - * The Neo4j active client connection. - * - * @var ClientInterface - */ - protected $neo; - - /** - * The Neo4j database transaction. - * - * @var TransactionInterface - */ - protected $transaction; - - /** - * Default connection configuration parameters. - * - * @var array - */ - protected $defaults = array( - 'scheme' => 'bolt', - 'host' => 'localhost', - 'port' => 7687, - 'username' => null, - 'password' => null, - ); - - /** - * The neo4j driver name. - * - * @var string - */ - protected $driverName = 'neo4j'; - - /** - * Create a new database connection instance. - * - * @param array $config The database connection configuration - */ - public function __construct(array $config = []) - { - $this->config = $config; - } + /** @var UnmanagedTransactionInterface[] */ + private array $activeTransactions = []; - /** - * Set the query grammar used by the connection. - * - * @param \Illuminate\Database\Query\Grammars\Grammar $grammar - */ - public function setQueryGrammar(Grammar $grammar) - { - $this->queryGrammar = $grammar; - } + private SummaryCounters $totals; - /** - * Set the query grammar to the default implementation. - */ - public function useDefaultQueryGrammar() + public function escape($value, $binary = false) { - $this->queryGrammar = $this->getDefaultQueryGrammar(); + throw new RuntimeException('Escaping from the connection is not supported yet.'); } - /** - * Set the schema grammar to the default implementation. - */ - public function useDefaultSchemaGrammar() + public function hasModifiedRecords(): bool { - $this->schemaGrammar = $this->getDefaultSchemaGrammar(); + return $this->totals->containsUpdates(); } - /** - * Set the query post processor to the default implementation. - */ - public function useDefaultPostProcessor() + public function recordsHaveBeenModified($value = true) { - $this->postProcessor = $this->getDefaultPostProcessor(); + throw new RuntimeException('Record modification is handled by summary totals in this connection.'); } - /** - * Get the query post processor used by the connection. - * - * @return \Illuminate\Database\Query\Processors\Processor - */ - public function getPostProcessor() + public function setRecordModificationState(bool $value) { - return $this->postProcessor; + throw new RuntimeException('Record modification is handled by summary totals in this connection.'); } - /** - * Set the query post processor used by the connection. - * - * @param \Illuminate\Database\Query\Processors\Processor $processor - */ - public function setPostProcessor(Processor $processor) + public function forgetRecordModificationState(): void { - $this->postProcessor = $processor; + $this->totals = new SummaryCounters(); } - /** - * Get the event dispatcher used by the connection. - * - * @return Dispatcher - */ - public function getEventDispatcher() + public function getPdo(): SessionInterface { - return $this->events; + return $this->session; } - /** - * Get the default post processor instance. - * - * @return \Illuminate\Database\Query\Processors\Processor - */ - protected function getDefaultPostProcessor() + public function getRawPdo(): SessionInterface { - return new Processor(); + return $this->session; } - /** - * Determine if the connection in a "dry run". - * - * @return bool - */ - public function pretending() - { - return $this->pretending === true; - } - - // * - // * Get the default fetch mode for the connection. - // * - // * @return int - - // public function getFetchMode() - // { - // return $this->fetchMode; - // } - - /** - * Clear the query log. - */ - public function flushQueryLog() - { - $this->queryLog = []; - } - - /** - * Enable the query log on the connection. - */ - public function enableQueryLog() - { - $this->loggingQueries = true; - } - - /** - * Disable the query log on the connection. - */ - public function disableQueryLog() - { - $this->loggingQueries = false; - } - - /** - * Get the connection query log. - * - * @return array - */ - public function getQueryLog() + public function getReadPdo(): SessionInterface { - return $this->queryLog; + return $this->readSession; } - /** - * Determine whether we're logging queries. - * - * @return bool - */ - public function logging() + public function getRawReadPdo(): SessionInterface { - return $this->loggingQueries; + return $this->readSession; } - /** - * Set the default fetch mode for the connection. - * - * @param int $fetchMode - * - * @return int - */ - public function setFetchMode($fetchMode) - { - $this->fetchMode = $fetchMode; - } + public function __construct( + private readonly SessionInterface $readSession, + private readonly SessionInterface $session, + string $database, + string $tablePrefix, + array $config + ) { + parent::__construct(static fn () => throw new LogicException('Cannot use PDO in '.self::class), $database, $tablePrefix, $config); - /** - * Set the event dispatcher instance on the connection. - * - * @param Dispatcher $events - */ - public function setEventDispatcher(Dispatcher $events) - { - $this->events = $events; - } + $this->useDefaultSchemaGrammar(); - /** - * Get a new raw query expression. - * - * @param mixed $value - * - * @return \Illuminate\Database\Query\Expression - */ - public function raw($value) - { - return new Expression($value); + $this->totals = new SummaryCounters(); } - /** - * Run a select statement and return a single result. - * - * @param string $query - * @param array $bindings - * - * @return mixed - */ - public function selectOne($query, $bindings = []) + protected function getDefaultSchemaGrammar(): SchemaGrammar { - $records = $this->select($query, $bindings); - - return count($records) > 0 ? reset($records) : null; + return new SchemaGrammar(); } - /** - * Run a select statement against the database. - * - * @param string $query - * @param array $bindings - * - * @return array - */ - public function selectFromWriteConnection($query, $bindings = []) + protected function getDefaultQueryGrammar(): CypherGrammar { - return $this->select($query, $bindings, false); + return new CypherGrammar(); } - public function createConnection() + public function query(): Query\Builder { - return $this->getClient(); + return new Query\Builder( + $this, $this->getQueryGrammar(), $this->getPostProcessor() + ); } - /** - * Create a new Neo4j client. - * - * @return ClientInterface - */ - public function createSingleConnectionClient() + public function getSchemaBuilder(): Builder { - return $this->initBuilder() - ->withDriver('default', $this->buildUriFromConfig($this->getConfig()), $this->getAuth()) - ->build(); + return new Builder($this); } - private function initBuilder(): ClientBuilder + protected function getDefaultPostProcessor(): Processor { - $formatter = new SummarizedResultFormatter(OGMFormatter::create()); - return ClientBuilder::create()->withFormatter($formatter); + return new Processors\Processor(); } - - public function createMultipleConnectionsClient() + public function getSession(bool $read = false): SessionInterface { - $builder = $this->initBuilder(); - - $default = $this->getConfigOption('default'); - - foreach ($this->getConfigOption('connections') as $connection => $config) { - if ($default === $connection) { - $builder = $builder->withDefaultDriver($connection); - } - - $builder = $builder->withDriver($connection, $this->buildUriFromConfig($config), $this->getAuth()); + if ($read) { + return $this->readSession; } - return $builder->build(); + return $this->session; } - /** - * Get the currenty active database client. - * - * @return ClientInterface - */ - public function getClient() + public function getRunner(bool $read = false): TransactionInterface { - if (!$this->neo) { - $this->setClient($this->createSingleConnectionClient()); + if (count($this->activeTransactions)) { + return Arr::last($this->activeTransactions); } - return $this->neo; - } - - /** - * Set the client responsible for the - * database communication. - * - * @param ClientInterface $client - */ - public function setClient(ClientInterface $client) - { - $this->neo = $client; - } - - public function getScheme(array $config) - { - return Arr::get($config, 'scheme', $this->defaults['scheme']); - } - - /** - * Get the connection host. - * - * @return string - */ - public function getHost(array $config) - { - return Arr::get($config, 'host', $this->defaults['host']); - } - - /** - * Get the connection port. - * - * @return int|string - */ - public function getPort(array $config) - { - return Arr::get($config, 'port', $this->defaults['port']); + return $this->getSession($read); } - /** - * Get the connection username. - * - * @return int|string - */ - public function getUsername(array $config) + protected function run($query, $bindings, Closure $callback): mixed { - return Arr::get($config, 'username', $this->defaults['username']); - } + $autobound = CypherGrammar::getBindings($query); + if (count($autobound) === 0) { + CypherGrammar::setBindings($query, $bindings); + } else { + $bindings = $autobound; + } + foreach ($this->beforeExecutingCallbacks as $beforeExecutingCallback) { + $beforeExecutingCallback($query, $bindings, $this); + } - /** - * Returns whether or not the connection should be secured. - * - * @return bool - */ - public function isSecured(array $config) - { - return Arr::get($config, 'username') !== null && Arr::get($config, 'password') !== null; - } + $this->reconnectIfMissingConnection(); - /** - * Get the connection password. - * - * @return int|strings - */ - public function getPassword(array $config) - { - return Arr::get($config, 'password', $this->defaults['password']); - } + $start = (int) microtime(true); - public function getConfig() - { - return $this->config; - } + try { + $result = $callback($query, $bindings); + } catch (Throwable $e) { + throw new QueryException('bolt', $query, $bindings, $e); + } - /** - * Get an option from the configuration options. - * - * @param string $option - * @param mixed $default - * - * @return mixed - */ - public function getConfigOption($option, $default = null) - { - return Arr::get($this->getConfig(), $option, $default); - } + $this->logQuery($query, $bindings, $this->getElapsedTime($start)); - /** - * Get the database connection name. - * - * @return string|null - */ - public function getName() - { - return $this->getConfigOption('name'); + return $result; } - /** - * Get the Neo4j driver name. - * - * @return string - */ - public function getDriverName() + public function scalar($query, $bindings = [], $useReadPdo = true) { - return $this->driverName; + return Arr::first($this->selectOne($query, $bindings, $useReadPdo) ?? []); } - /** - * Run a select statement against the database. - * - * @param string $query - * @param array $bindings - * - * @return SummarizedResult - */ - public function select($query, $bindings = array()) + public function cursor($query, $bindings = [], $useReadPdo = true): Generator { - return $this->run($query, $bindings, function (self $me, $query, array $bindings) { - if ($me->pretending()) { - return array(); + return $this->run($query, $bindings, function (string $query, $bindings) use ($useReadPdo) { + if ($this->pretending) { + return; } - // For select statements, we'll simply execute the query and return an array - // of the database result set. Each element in the array will be a single - // node from the database, and will either be an array or objects. - $query = $me->getCypherQuery($query, $bindings); - - /** @var SummarizedResult $results */ - return $this->getClient()->run($query['statement'], $query['parameters']); + $statement = new Statement($query, $bindings); + /** + * @noinspection PhpParamsInspection + * + * @psalm-suppress InvalidArgument + */ + $this->event(new StatementPrepared($this, $statement)); + + yield from $this->getRunner($useReadPdo) + ->runStatement($statement) + ->map(static fn (CypherMap $map) => $map->toArray()); }); } - /** - * Run an insert statement against the database. - * - * @param string $query - * @param array $bindings - * - * @return mixed - */ - public function insert($query, $bindings = array()) + public function select($query, $bindings = [], $useReadPdo = true): array { - return $this->statement($query, $bindings, true); - } - - /** - * Run an update statement against the database. - * - * @param string $query - * @param array $bindings - * - * @return SummarizedResult - */ - public function update($query, $bindings = []) - { - return $this->affectingStatement($query, $bindings); + try { + return iterator_to_array($this->cursor($query, $bindings, $useReadPdo)); + } catch (Neo4jException $e) { + throw new QueryException($this->getName() ?? '', $query, $bindings, $e); + } } - /** - * Run a delete statement against the database. - * - * @param string $query - * @param array $bindings - * - * @return int - */ - public function delete($query, $bindings = []) + public function statement($query, $bindings = []): bool { - return $this->affectingStatement($query, $bindings); + return (bool) $this->affectingStatement($query, $bindings); } - /** - * Run a Cypher statement and get the number of nodes affected. - * - * @param string $query - * @param array $bindings - * - * @return SummarizedResult - */ - public function affectingStatement($query, $bindings = array()) + public function selectResultSets($query, $bindings = [], $useReadPdo = true): array { - return $this->run($query, $bindings, function (self $me, $query, array $bindings) { - if ($me->pretending()) { - return 0; - } - - // For update or delete statements, we want to get the number of rows affected - // by the statement and return that back to the developer. We'll first need - // to execute the statement and then we'll use CypherQuery to fetch the affected. - $query = $me->getCypherQuery($query, $bindings); - - /** @var SummarizedResult $summarizedResult */ - return $this->getClient()->writeTransaction(static function (TransactionInterface $tsx) use ($query) { - return $tsx->run($query['statement'], $query['parameters']); - }); + return $this->run($query, $bindings, function (string $query) use ($useReadPdo) { + return [ + $this->select($query, useReadPdo: $useReadPdo), + ]; }); } - /** - * Execute a Cypher statement and return the boolean result. - * - * @param string $query - * @param array $bindings - * - * @return CypherList|bool - */ - public function statement($query, $bindings = array(), $rawResults = false) + public function affectingStatement($query, $bindings = []): int { - return $this->run($query, $bindings, function (self $me, $query, array $bindings) use ($rawResults) { - if ($me->pretending()) { + return $this->run($query, $bindings, function (string $query) { + if ($this->pretending) { return true; } - $query = $me->getCypherQuery($query, $bindings); - - /** @var SummarizedResult $run */ - $results = $this->getClient()->run($query['statement'], $query['parameters']); + $result = $this->getRunner()->run($query, CypherGrammar::getBindings($query)); - return ($rawResults === true) ? $results : true; + return $this->summarizeCounters($result->getSummary()->getCounters()); }); } - /** - * Run a raw, unprepared query against the PDO connection. - * - * @param string $query - * - * @return bool - */ - public function unprepared($query) + public function unprepared($query): bool { - return $this->run($query, [], function ($me, $query) { - if ($me->pretending()) { + return $this->run($query, [], function (string $query) { + if ($this->pretending) { return true; } - $this->getClient()->run($query); + $result = $this->getRunner()->run($query); + $change = $this->summarizeCounters($result->getSummary()->getCounters()) > 0; + + $this->recordsHaveBeenModified($change); return true; }); } - /** - * Make a query out of a Cypher statement - * and the bindings values. - * - * @param string $query - * @param array $bindings - */ - public function getCypherQuery($query, array $bindings) + public function insert($query, $bindings = []): bool { - return ['statement' => $query, 'parameters' => $this->prepareBindings($bindings)]; + return (bool) $this->affectingStatement($query); } /** * Prepare the query bindings for execution. - * - * @param array $bindings - * - * @return array */ - public function prepareBindings(array $bindings) + public function prepareBindings(array $bindings): array { - $grammar = $this->getQueryGrammar(); - - $prepared = array(); - - foreach ($bindings as $key => $binding) { - // The bindings are collected in a little bit different way than - // Eloquent, we will need the key name in order to know where to replace - // the value using the Neo4j client. - $value = $binding; - - // We need to get the array value of the binding - // if it were mapped - if (is_array($value)) { - // There are different ways to handle multiple - // bindings vs. single bindings as values. - $value = array_values($value); - } - - // We need to transform all instances of the DateTime class into an actual - // date string. Each query grammar maintains its own date string format - // so we'll just ask the grammar for the format to get from the date. + $tbr = []; - if ($value instanceof DateTime) { - $binding = $value->format($grammar->getDateFormat()); - } - - // We will set the binding key and value, then - // we replace the binding property of the id (if found) - // with a _nodeId instead since the client - // will not accept replacing "id(n)" with a value - // which have been previously processed by the grammar - // to be _nodeId instead. - if (!is_array($binding)) { - $binding = [$binding]; - } - - foreach ($binding as $property => $real) { - // We should not pass any numeric key-value items since the Neo4j client expects - // a JSON dictionary. - if (is_numeric($property)) { - $property = (!is_numeric($key)) ? $key : 'id'; - } - - if ($property == 'id') { - $property = $grammar->getIdReplacement($property); - } - - // when the value is an array means we have - // a property as an array so we'll - // keep adding to it. - if (is_array($value)) { - $prepared[$property][] = $real; - } else { - $prepared[$property] = $real; - } + foreach ($bindings as $key => $value) { + if (is_int($key)) { + $tbr['param'.$key] = $value; + } else { + $tbr[$key] = $value; } } - return $prepared; + return $tbr; } - /** - * Get the query grammar used by the connection. - * - * @return CypherGrammar - */ - public function getQueryGrammar() + public function beginTransaction(): void { - if (!$this->queryGrammar) { - $this->useDefaultQueryGrammar(); - } - - return $this->queryGrammar; - } - - /** - * Get the default query grammar instance. - * - * @return CypherGrammar - */ - protected function getDefaultQueryGrammar() - { - return new Query\Grammars\CypherGrammar(); - } - - /** - * A binding should always be in an associative - * form of a key=>value, otherwise we will not be able to - * consider it a valid binding and replace its values in the query. - * This function validates whether the binding is valid to be used. - * - * @param array $binding - * - * @return bool - */ - public function isBinding(array $binding) - { - if (!empty($binding)) { - // A binding is valid only when the key is not a number - $keys = array_keys($binding); - - return !is_numeric(reset($keys)); - } - - return false; - } - - /** - * Execute a Closure within a transaction. - * - * @param Closure $callback - * - * @return mixed - * - * @throws Throwable - */ - public function transaction(Closure $callback, $attempts = 1) - { - $this->beginTransaction(); - - // We'll simply execute the given callback within a try / catch block - // and if we catch any exception we can rollback the transaction - // so that none of the changes are persisted to the database. - try { - $result = $callback($this); + $this->activeTransactions[] = $this->getSession()->beginTransaction(); - $this->commit(); - } - - // If we catch an exception, we will roll back so nothing gets messed - // up in the database. Then we'll re-throw the exception so it can - // be handled how the developer sees fit for their applications. - catch (Exception $e) { - $this->rollBack(); - - throw $e; - } catch (Throwable $e) { - $this->rollBack(); - - throw $e; - } + $this->transactionsManager->begin($this->getName() ?? '', $this->transactions); - return $result; + $this->fireConnectionEvent('beganTransaction'); } - /** - * Start a new database transaction. - */ - public function beginTransaction() + public function commit(): void { - ++$this->transactions; - - if ($this->transactions == 1) { - $client = $this->getClient(); - $this->transaction = $client->beginTransaction(); - } + $this->fireConnectionEvent('committing'); - $this->fireConnectionEvent('beganTransaction'); - } + $this->popTransaction()?->commit(); - /** - * Commit the active database transaction. - */ - public function commit() - { - if ($this->transactions == 1) { - $this->transaction->commit(); + if ($this->afterCommitCallbacksShouldBeExecuted()) { + $this->transactionsManager->commit($this->getName() ?? ''); } - --$this->transactions; - $this->fireConnectionEvent('committed'); } - /** - * Get the number of active transactions. - * - * @return int - */ - public function transactionLevel() + private function popTransaction(): ?UnmanagedTransactionInterface { - return $this->transactions; + return count($this->activeTransactions) ? array_pop($this->activeTransactions) : null; } - /** - * Rollback the active database transaction. - */ - public function rollBack() + public function rollBack($toLevel = null): void { - if ($this->transactions == 1) { - $this->transactions = 0; - - $this->transaction->rollback(); - } else { - --$this->transactions; + if (count($this->activeTransactions) === 0) { + return; } - $this->fireConnectionEvent('rollingBack'); - } - - /** - * Execute the given callback in "dry run" mode. - * - * @param Closure $callback - * - * @return array - */ - public function pretend(Closure $callback) - { - $loggingQueries = $this->loggingQueries; - - $this->enableQueryLog(); - - $this->pretending = true; - - $this->queryLog = []; - - // Basically to make the database connection "pretend", we will just return - // the default values for all the query methods, then we will return an - // array of queries that were "executed" within the Closure callback. - $callback($this); - - $this->pretending = false; + $this->popTransaction()?->rollback(); - $this->loggingQueries = $loggingQueries; - - return $this->queryLog; + $this->fireConnectionEvent('rollingBack'); } - /** - * Begin a fluent query against a node. - * - * @param string $label - * - * @return QueryBuilder - */ - public function node($label) + public function transactionLevel(): int { - $query = new QueryBuilder($this, $this->getQueryGrammar()); - - return $query->from($label); + return count($this->activeTransactions); } - /** - * Get a new query builder instance. - * - * @return \Illuminate\Database\Query\Builder - */ - public function query() + private function summarizeCounters(SummaryCounters $counters): int { - return new QueryBuilder( - $this, $this->getQueryGrammar(), $this->getPostProcessor() - ); + return $counters->propertiesSet() + + $counters->labelsAdded() + + $counters->labelsRemoved() + + $counters->nodesCreated() + + $counters->nodesDeleted() + + $counters->relationshipsCreated() + + $counters->relationshipsDeleted(); } - /** - * Run a Cypher statement and log its execution context. - * - * @param string $query - * @param array $bindings - * @param Closure $callback - * - * @return mixed - * - * @throws QueryException - */ - protected function run($query, $bindings, Closure $callback) + public function selectOne($query, $bindings = [], $useReadPdo = true): array|null { - $start = microtime(true); - - // To execute the statement, we'll simply call the callback, which will actually - // run the Cypher against the Neo4j connection. Then we can calculate the time it - // took to execute and log the query Cypher, bindings and time in our memory. - try { - $result = $callback($this, $query, $bindings); - } - - // If an exception occurs when attempting to run a query, we'll format the error - // message to include the bindings with Cypher, which will make this exception a - // lot more helpful to the developer instead of just the database's errors. - catch (Exception $e) { - $this->handleExceptions($query, $bindings, $e); + foreach ($this->cursor($query, useReadPdo: $useReadPdo) as $result) { + return $result; } - // Once we have run the query we will calculate the time that it took to run and - // then log the query, bindings, and execution time so we will report them on - // the event that the developer needs them. We'll log time in milliseconds. - $time = $this->getElapsedTime($start); - - $this->logQuery($query, $bindings, $time); - - return $result; + return null; } - /** - * Run a Cypher statement. - * - * @param string $query - * @param array $bindings - * @param Closure $callback - * - * @return mixed - * - * @throws InvalidCypherException - */ - protected function runQueryCallback($query, $bindings, Closure $callback) + public function transaction(Closure $callback, $attempts = 1): mixed { - // To execute the statement, we'll simply call the callback, which will actually - // run the SQL against the PDO connection. Then we can calculate the time it - // took to execute and log the query SQL, bindings and time in our memory. - try { - $result = $callback($this, $query, $bindings); - } - - // If an exception occurs when attempting to run a query, we'll format the error - // message to include the bindings with SQL, which will make this exception a - // lot more helpful to the developer instead of just the database's errors. - catch (Exception $e) { - throw new QueryException( - $query, $this->prepareBindings($bindings), $e - ); - } + for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) { + $this->beginTransaction(); - return $result; - } + try { + /** @psalm-suppress ArgumentTypeCoercion */ + $callbackResult = $callback($this); + } catch (Neo4jException $e) { + if ($e->getClassification() === 'Transaction') { + continue; + } else { + throw $e; + } + } - /** - * Handle a query exception that occurred during query execution. - * - * @param Exceptions\Exception $e - * @param string $query - * @param array $bindings - * @param Closure $callback - * - * @return mixed - * - * @throws Exceptions\Exception - */ - protected function tryAgainIfCausedByLostConnection(QueryException $e, $query, $bindings, Closure $callback) - { - if ($this->causedByLostConnection($e->getPrevious())) { - $this->reconnect(); + $this->commit(); - return $this->runQueryCallback($query, $bindings, $callback); + /** @psalm-suppress PossiblyUndefinedVariable */ + return $callbackResult; } - throw $e; + return null; } - /** - * Determine if the given exception was caused by a lost connection. - * - * @param \Illuminate\Database\QueryException - * @return bool - */ - protected function causedByLostConnection(QueryException $e) + public function bindValues($statement, $bindings) { - return str_contains($e->getPrevious()->getMessage(), 'server has gone away'); } - - /** - * Disconnect from the underlying PDO connection. - */ - public function disconnect() - { - $this->neo = null; - } - - /** - * Reconnect to the database. - * - * - * @throws LogicException - */ public function reconnect() { - if (is_callable($this->reconnector)) { - return call_user_func($this->reconnector, $this); - } - - throw new LogicException('Lost connection and no reconnector available.'); + throw new LostConnectionException('Lost connection and no reconnector available.'); } - /** - * Reconnect to the database if a PDO connection is missing. - */ - protected function reconnectIfMissingConnection() + public function reconnectIfMissingConnection(): void { - if (is_null($this->getClient())) { - $this->reconnect(); - } } - /** - * Log a query in the connection's query log. - * - * @param string $query - * @param array $bindings - * @param float|null $time - */ - public function logQuery($query, $bindings, $time = null) + public function disconnect(): void { - if (isset($this->events)) { - $this->events->dispatch('illuminate.query', [$query, $bindings, $time, $this->getName()]); - } - - if ($this->loggingQueries) { - $this->queryLog[] = compact('query', 'bindings', 'time'); - } } - /** - * Register a database query listener with the connection. - * - * @param Closure $callback - */ - public function listen(Closure $callback) + public function isDoctrineAvailable(): bool { - if (isset($this->events)) { - $this->events->listen(Events\QueryExecuted::class, $callback); - } - } - - /** - * Fire an event for this connection. - * - * @param string $event - */ - protected function fireConnectionEvent($event) - { - if (isset($this->events)) { - $this->events->dispatch('connection.'.$this->getName().'.'.$event, $this); - } - } - - /** - * Get the elapsed time since a given starting point. - * - * @param int $start - * - * @return float - */ - protected function getElapsedTime($start) - { - return round((microtime(true) - $start) * 1000, 2); - } - - /** - * Set the reconnect instance on the connection. - * - * @param callable $reconnector - * - * @return $this - */ - public function setReconnector(callable $reconnector) - { - $this->reconnector = $reconnector; - - return $this; - } - - /** - * Set the schema grammar used by the connection. - * - * @param \Illuminate\Database\Schema\Grammars\Grammar - */ - public function setSchemaGrammar(SchemaGrammar $grammar) - { - $this->schemaGrammar = $grammar; - } - - /** - * Get the schema grammar used by the connection. - * - * @return \Illuminate\Database\Query\Grammars\Grammar - */ - public function getSchemaGrammar() - { - return $this->schemaGrammar; + return false; } - /** - * Get the default schema grammar instance. - * - * @return \Illuminate\Database\Schema\Grammars\Grammar - */ - protected function getDefaultSchemaGrammar() + public function usingNativeSchemaOperations(): bool { + return true; } - /** - * Get a schema builder instance for the connection. - * - * @return Builder - */ - public function getSchemaBuilder() + public function getDoctrineColumn($table, $column) { - if (is_null($this->schemaGrammar)) { - $this->useDefaultSchemaGrammar(); - } - - return new Schema\Builder($this); + throw new BadMethodCallException('Cannot use doctrine on graph databases'); } - /** - * Handle exceptions thrown in $this::run() - * - * @throws mixed - */ - protected function handleExceptions($query, $bindings, $e) + public function getDoctrineSchemaManager() { - throw new QueryException($query, $bindings, $e); + throw new BadMethodCallException('Cannot use doctrine on graph databases'); } - /** - * @return string - */ - private function buildUriFromConfig(array $config): string + public function getDoctrineConnection() { - $uri = ''; - $scheme = $this->getScheme($config); - if ($scheme) { - $uri .= $scheme . '://'; - } - - $host = $this->getHost($config); - if ($host) { - $uri .= '@' . $host; - } - - $port = $this->getPort($config); - if ($port) { - $uri .= ':' . $port; - } - - return $uri; + throw new BadMethodCallException('Cannot use doctrine on graph databases'); } - /** - * @return AuthenticateInterface - */ - private function getAuth(): AuthenticateInterface + public function registerDoctrineType(Type|string $class, string $name, string $type): void { - $username = $this->getUsername($this->getConfig()); - $password = $this->getPassword($this->getConfig()); - if ($username && $password) { - return Authenticate::basic($username, $password); - } - - return Authenticate::disabled(); + throw new BadMethodCallException('Cannot use doctrine on graph databases'); } } diff --git a/src/ConnectionAdapter.php b/src/ConnectionAdapter.php deleted file mode 100644 index 34c0932d..00000000 --- a/src/ConnectionAdapter.php +++ /dev/null @@ -1,634 +0,0 @@ -neoeloquent = app('neoeloquent.connection'); - } - - - /** - * Set the query grammar to the default implementation. - * - * @return void - */ - public function useDefaultQueryGrammar() - { - $this->neoeloquent->useDefaultQueryGrammar(); - } - - /** - * Get the default query grammar instance. - * - * @return \Illuminate\Database\Query\Grammars\Grammar - */ - protected function getDefaultQueryGrammar() - { - return $this->neoeloquent->getDefaultQueryGrammar(); - } - - /** - * Set the schema grammar to the default implementation. - * - * @return void - */ - public function useDefaultSchemaGrammar() - { - $this->neoeloquent->useDefaultSchemaGrammar(); - } - - /** - * Get the default schema grammar instance. - * - * @return \Illuminate\Database\Schema\Grammars\Grammar - */ - protected function getDefaultSchemaGrammar() {} - - /** - * Set the query post processor to the default implementation. - * - * @return void - */ - public function useDefaultPostProcessor() - { - $this->neoeloquent->useDefaultQueryGrammar(); - } - - /** - * Get the default post processor instance. - * - * @return \Illuminate\Database\Query\Processors\Processor - */ - protected function getDefaultPostProcessor() - { - return $this->neoeloquent->getDefaultPostProcessor(); - } - - /** - * Get a schema builder instance for the connection. - * - * @return \Illuminate\Database\Schema\Builder - */ - public function getSchemaBuilder() - { - return $this->neoeloquent->getSchemaBuilder(); - } - - /** - * Get a new raw query expression. - * - * @param mixed $value - * @return \Illuminate\Database\Query\Expression - */ - public function raw($value) - { - return $this->neoeloquent->raw($value); - } - - /** - * Run a select statement and return a single result. - * - * @param string $query - * @param array $bindings - * @return mixed - */ - public function selectOne($query, $bindings = array(), $useReadPdo = true) - { - return $this->neoeloquent->selectOne($query, $bindings); - } - - /** - * Run a select statement against the database. - * - * @param string $query - * @param array $bindings - * @return array - */ - public function selectFromWriteConnection($query, $bindings = array()) - { - return $this->neoeloquent->selectFromWriteConnection($query, $bindings); - } - - /** - * Run a select statement against the database. - * - * @param string $query - * @param array $bindings - * @param bool $useReadPdo - * @return array - */ - public function select($query, $bindings = array(), $useReadPdo = true) - { - return $this->neoeloquent->select($query, $bindings); - } - - /** - * Run an insert statement against the database. - * - * @param string $query - * @param array $bindings - * @return bool - */ - public function insert($query, $bindings = array()) - { - return $this->neoeloquent->insert($query, $bindings); - } - - /** - * Run an update statement against the database. - * - * @param string $query - * @param array $bindings - * @return int - */ - public function update($query, $bindings = array()) - { - return $this->neoeloquent->update($query, $bindings); - } - - /** - * Run a delete statement against the database. - * - * @param string $query - * @param array $bindings - * @return int - */ - public function delete($query, $bindings = array()) - { - return $this->neoeloquent->delete($query, $bindings); - } - - /** - * Execute an SQL statement and return the boolean result. - * - * @param string $query - * @param array $bindings - * @return bool - */ - public function statement($query, $bindings = array(), $rawResults = false) - { - return $this->neoeloquent->statement($query, $bindings, $rawResults); - } - - /** - * Run an SQL statement and get the number of rows affected. - * - * @param string $query - * @param array $bindings - * @return int - */ - public function affectingStatement($query, $bindings = array()) - { - return $this->neoeloquent->affectingStatement($query, $bindings); - } - - /** - * Run a raw, unprepared query against the PDO connection. - * - * @param string $query - * @return bool - */ - public function unprepared($query) - { - return $this->neoeloquent->unprepared($query); - } - - /** - * Prepare the query bindings for execution. - * - * @param array $bindings - * @return array - */ - public function prepareBindings(array $bindings) - { - return $this->neoeloquent->prepareBindings($bindings); - } - - /** - * Execute a Closure within a transaction. - * - * @param \Closure $callback - * @return mixed - * - * @throws \Exception - */ - public function transaction(Closure $callback, $attempts = 1) - { - $this->neoeloquent->transaction($callback, $attempts); - } - - /** - * Start a new database transaction. - * - * @return void - */ - public function beginTransaction() - { - $this->neoeloquent->beginTransaction(); - } - - /** - * Commit the active database transaction. - * - * @return void - */ - public function commit() - { - $this->neoeloquent->commit(); - } - - /** - * Rollback the active database transaction. - * - * @return void - */ - public function rollBack($toLevel = null) - { - $this->neoeloquent->rollBack(); - } - - /** - * Get the number of active transactions. - * - * @return int - */ - public function transactionLevel() - { - return $this->neoeloquent->transactionLevel(); - } - - /** - * Execute the given callback in "dry run" mode. - * - * @param \Closure $callback - * @return array - */ - public function pretend(Closure $callback) - { - return $this->neoeloquent->pretend($callback); - } - - /** - * Run a SQL statement and log its execution context. - * - * @param string $query - * @param array $bindings - * @param \Closure $callback - * @return mixed - * - * @throws \Illuminate\Database\QueryException - */ - protected function run($query, $bindings, Closure $callback) - { - return $this->neoeloquent->run($query, $bindings, $callback); - } - - /** - * Run a SQL statement. - * - * @param string $query - * @param array $bindings - * @param \Closure $callback - * @return mixed - * - * @throws \Illuminate\Database\QueryException - */ - protected function runQueryCallback($query, $bindings, Closure $callback) - { - return $this->neoeloquent->runQueryCallback($query, $bindings, $callback); - } - - /** - * Handle a query exception that occurred during query execution. - * - * @param \Illuminate\Database\QueryException $e - * @param string $query - * @param array $bindings - * @param \Closure $callback - * @return mixed - * - * @throws \Illuminate\Database\QueryException - */ - protected function tryAgainIfCausedByLostConnection(IlluminateQueryException $e, $query, $bindings, Closure $callback) - { - return $this->neoeloquent->tryAgainIfCausedByLostConnection(new QueryException($e), $query, $bindings, $callback); - } - - /** - * Determine if the given exception was caused by a lost connection. - * - * @param \Illuminate\Database\QueryException - * @return bool - */ - protected function causedByLostConnection(Throwable $e) - { - return $this->neoeloquent->causedByLostConnection(new QueryException($e)); - } - - /** - * Disconnect from the underlying PDO connection. - * - * @return void - */ - public function disconnect() - { - $this->neoeloquent->disconnect(); - } - - /** - * Reconnect to the database. - * - * @return void - * - * @throws \LogicException - */ - public function reconnect() - { - $this->neoeloquent->reconnect(); - } - - /** - * Reconnect to the database if a PDO connection is missing. - * - * @return void - */ - protected function reconnectIfMissingConnection() - { - $this->neoeloquent->reconnectIfMissingConnection(); - } - - /** - * Log a query in the connection's query log. - * - * @param string $query - * @param array $bindings - * @param float|null $time - * @return void - */ - public function logQuery($query, $bindings, $time = null) - { - $this->neoeloquent->logQuery($query, $bindings, $time, null); - } - - /** - * Register a database query listener with the connection. - * - * @param \Closure $callback - * @return void - */ - public function listen(Closure $callback) - { - $this->neoeloquent->listen($callback); - } - - /** - * Fire an event for this connection. - * - * @param string $event - * @return void - */ - protected function fireConnectionEvent($event) - { - $this->neoeloquent->fireConnectionEvent($event); - } - - /** - * Get the elapsed time since a given starting point. - * - * @param int $start - * @return float - */ - protected function getElapsedTime($start) - { - return $this->neoeloquent->getElapsedTime($start); - } - - /** - * Set the reconnect instance on the connection. - * - * @param callable $reconnector - * @return $this - */ - public function setReconnector(callable $reconnector) - { - return $this->neoeloquent->setReconnector($reconnector); - } - - /** - * Get the database connection name. - * - * @return string|null - */ - public function getName() - { - return $this->neoeloquent->getConfigOption('name'); - } - - /** - * Get an option from the configuration options. - * - * @param string $option - * @return mixed - */ - public function getConfig($option = null) - { - return $this->neoeloquent->getConfigOption($option); - } - - /** - * Get the PDO driver name. - * - * @return string - */ - public function getDriverName() - { - return $this->neoeloquent->getDriverName(); - } - - /** - * Get the query grammar used by the connection. - * - * @return \Illuminate\Database\Query\Grammars\Grammar - */ - public function getQueryGrammar() - { - return $this->neoeloquent->getQueryGrammar(); - } - - /** - * Set the query grammar used by the connection. - * - * @param \Illuminate\Database\Query\Grammars\Grammar - * @return void - */ - public function setQueryGrammar(Grammar $grammar) - { - $this->neoeloquent->setQueryGrammar($grammar); - } - - /** - * Get the schema grammar used by the connection. - * - * @return \Illuminate\Database\Query\Grammars\Grammar - */ - public function getSchemaGrammar() - { - return $this->neoeloquent->getSchemaGrammar(); - } - - /** - * Set the schema grammar used by the connection. - * - * @param \Illuminate\Database\Schema\Grammars\Grammar - * @return void - */ - public function setSchemaGrammar(SchemaGrammar $grammar) - { - $this->neoeloquent->setSchemaGrammar($grammar); - } - - /** - * Get the query post processor used by the connection. - * - * @return \Illuminate\Database\Query\Processors\Processor - */ - public function getPostProcessor() - { - return $this->neoeloquent->getPostProcessor(); - } - - /** - * Set the query post processor used by the connection. - * - * @param \Illuminate\Database\Query\Processors\Processor - * @return void - */ - public function setPostProcessor(Processor $processor) - { - $this->neoeloquent->setPostProcessor($processor); - } - - /** - * Get the event dispatcher used by the connection. - * - * @return \Illuminate\Contracts\Events\Dispatcher - */ - public function getEventDispatcher() - { - return $this->neoeloquent->getEventDispatcher(); - } - - /** - * Set the event dispatcher instance on the connection. - * - * @param \Illuminate\Contracts\Events\Dispatcher - * @return void - */ - public function setEventDispatcher(IlluminateDispatcher $events) - { - $this->neoeloquent->setEventDispatcher(\App::make(Dispatcher::class)); - } - - /** - * Determine if the connection in a "dry run". - * - * @return bool - */ - public function pretending() - { - return $this->neoeloquent->pretending(); - } - - /** - * Get the default fetch mode for the connection. - * - * @return int - */ - public function getFetchMode() - { - return $this->fetchMode; - } - - /** - * Set the default fetch mode for the connection. - * - * @param int $fetchMode - * @return int - */ - public function setFetchMode($fetchMode, $fetchArgument = null, array $fetchConstructorArgument = []) - { - $this->neoeloquent->setFetchMode($fetchMode); - } - - /** - * Get the connection query log. - * - * @return array - */ - public function getQueryLog() - { - return $this->neoeloquent->getQueryLog(); - } - - /** - * Clear the query log. - * - * @return void - */ - public function flushQueryLog() - { - $this->neoeloquent->flushQueryLog(); - } - - /** - * Enable the query log on the connection. - * - * @return void - */ - public function enableQueryLog() - { - $this->neoeloquent->enableQueryLog(); - } - - /** - * Disable the query log on the connection. - * - * @return void - */ - public function disableQueryLog() - { - $this->neoeloquent->disableQueryLog(); - } - - /** - * Determine whether we're logging queries. - * - * @return bool - */ - public function logging() - { - return $this->neoeloquent->logging(); - } - - public function __call($method, $parameters) - { - call_user_func_array([$this->neoeloquent, $method], $parameters); - } -} diff --git a/src/ConnectionInterface.php b/src/ConnectionInterface.php deleted file mode 100644 index b08cf930..00000000 --- a/src/ConnectionInterface.php +++ /dev/null @@ -1,143 +0,0 @@ -container = $container; - } - - /** - * Establish a PDO connection based on the configuration. - * - * @param array $config - * @param string $name - * - * @return \Illuminate\Database\Connection - */ - public function make(array $config, $name = null) - { - if (isset($config['replication']) && $config['replication'] == true && isset($config['connections'])) { - // HA / Replication configuration - $connection = $this->createHAConnection($config); - } elseif (isset($config['connections']) && count($config['connections']) > 1) { - // multi-server configuration - $connection = $this->createMultiServerConnection($config); - } else { - // single connection configuration - $connection = $this->createSingleConnection($config); - } - - return $connection; - } - - /** - * Create a single database connection instance. - * - * @param array $config - * - * @return \Illuminate\Database\Connection - */ - protected function createSingleConnection(array $config) - { - $connector = $this->createConnector($config); - - return $this->createConnection($this->driver, $connector, Connection::TYPE_SINGLE, $config); - } - - /** - * Create a single database connection instance to multiple servers. - * - * @param array $config - * - * @return \Illuminate\Database\Connection - */ - protected function createMultiServerConnection(array $config) - { - $connector = $this->createConnector($config); - - return $this->createConnection($this->driver, $connector, Connection::TYPE_MULTI, $config); - } - - protected function createHAConnection(array $config) - { - $connector = $this->createConnector($config); - - return $this->createConnection($this->driver, $connector, Connection::TYPE_HA, $config); - } + private Uri $defaultUri; - /** - * Create a single database connection instance. - * - * @param array $config - * - * @return \Illuminate\Database\Connection - */ - protected function createReadWriteConnection(array $config) + public function __construct(Uri $defaultUri = null) { - $connection = $this->createSingleConnection($this->getWriteConfig($config)); - + $this->defaultUri = $defaultUri ?? Uri::create(); } /** - * Get the read configuration for a read / write connection. - * - * @param array $config + * @psalm-suppress MoreSpecificImplementedParamType + * @psalm-suppress ImplementedReturnTypeMismatch * - * @return array + * @param array{scheme?: string, driver: string, host?: string, port?: string|int, username ?: string, password ?: string, database ?: string, prefix ?: string} $config */ - protected function getReadConfig(array $config) + public function connect(array $config): Driver { - $readConfig = $this->getReadWriteConfig($config, 'read'); + $port = $config['port'] ?? null; + $port = (is_null($port) || $port === '') ? null : ((int) $port); + $uri = $this->defaultUri->withScheme($config['scheme'] ?? '') + ->withHost($config['host'] ?? '') + ->withPort($port); - if (isset($readConfig['host']) && is_array($readConfig['host'])) { - $readConfig['host'] = count($readConfig['host']) > 1 - ? $readConfig['host'][array_rand($readConfig['host'])] - : $readConfig['host'][0]; - } - - return $this->mergeReadWriteConfig($config, $readConfig); - } - - /** - * Merge a configuration for a read / write connection. - * - * @param array $config - * @param array $merge - * - * @return array - */ - protected function mergeReadWriteConfig(array $config, array $merge) - { - return Arr::except(array_merge($config, $merge), ['read', 'write']); - } - - /** - * Parse and prepare the database configuration. - * - * @param array $config - * @param string $name - * - * @return array - */ - protected function parseConfig(array $config, $name) - { - return Arr::add($config, 'name', $name); - } - - /** - * Create a connector instance based on the configuration. - * - * @param array $config - * - * @return \Illuminate\Database\Connectors\ConnectorInterface - * - * @throws \InvalidArgumentException - */ - public function createConnector(array $config) - { - if ($this->container->bound($key = "db.connector.{$this->driver}")) { - return $this->container->make($key); - } - - switch ($this->driver) { - case 'neo4j': - return new Neo4jConnector(); - break; - } - - throw new InvalidArgumentException("Unsupported driver [{$this->driver}]"); - } - - /** - * Create a new connection instance. - * - * @param string $driver - * @param \PDO|\Closure $connection - * @param string $database - * @param string $prefix - * @param array $config - * - * @return \Illuminate\Database\Connection - * - * @throws \InvalidArgumentException - */ - protected function createConnection($driver, $connector, $type, array $config = []) - { - if ($this->container->bound($key = "db.connection.{$driver}")) { - return $this->container->make($key, [$connection, $config]); - } - - switch ($driver) { - case 'neo4j': - return $connector->connect($type, $config); - break; + if (array_key_exists('username', $config) && array_key_exists('password', $config)) { + $auth = Authenticate::basic($config['username'], $config['password']); + } else { + $auth = Authenticate::disabled(); } - throw new InvalidArgumentException("Unsupported driver [$driver]"); + return Driver::create($uri, DriverConfiguration::default(), $auth); } } diff --git a/src/Connectors/Neo4jConnector.php b/src/Connectors/Neo4jConnector.php deleted file mode 100644 index 6ca0f78d..00000000 --- a/src/Connectors/Neo4jConnector.php +++ /dev/null @@ -1,36 +0,0 @@ -createSingleConnectionClient($config); - break; - - case Connection::TYPE_MULTI: - $client = $connection->createMultipleConnectionsClient($config); - break; - - case Connection::TYPE_HA: - throw new \Exception('High Availability mode is not supported anymore. Please use the neo4j scheme instead'); - break; - default: - throw new Exception('Unsupported connection type '+$type); - break; - } - - $connection->setClient($client); - - return $connection; - } -} diff --git a/src/Console/Migrations/BaseCommand.php b/src/Console/Migrations/BaseCommand.php deleted file mode 100644 index 0dd14ce0..00000000 --- a/src/Console/Migrations/BaseCommand.php +++ /dev/null @@ -1,54 +0,0 @@ -input->getOption('path'); - - // First, we will check to see if a path option has been defined. If it has - // we will use the path relative to the root of this installation folder - // so that migrations may be run for any path within the applications. - if (!is_null($path)) { - return $this->laravel['path.base'].'/'.$path; - } - - $package = $this->input->getOption('package'); - - // If the package is in the list of migration paths we received we will put - // the migrations in that path. Otherwise, we will assume the package is - // is in the package directories and will place them in that location. - if (!is_null($package)) { - return $this->packagePath.'/'.$package.'/src/'.self::LABELS_DIRECTORY; - } - - $bench = $this->input->getOption('bench'); - - // Finally we will check for the workbench option, which is a shortcut into - // specifying the full path for a "workbench" project. Workbenches allow - // developers to develop packages along side a "standard" app install. - if (!is_null($bench)) { - $path = "/workbench/{$bench}/src/".self::LABELS_DIRECTORY; - - return $this->laravel['path.base'].$path; - } - - return $this->laravel['path.database'].'/'.self::LABELS_DIRECTORY; - } -} diff --git a/src/Console/Migrations/MigrateCommand.php b/src/Console/Migrations/MigrateCommand.php deleted file mode 100644 index 87388f9f..00000000 --- a/src/Console/Migrations/MigrateCommand.php +++ /dev/null @@ -1,102 +0,0 @@ -migrator = $migrator; - $this->packagePath = $packagePath; - } - - /** - * {@inheritDoc} - */ - public function fire() - { - if (!$this->confirmToProceed()) { - return; - } - - // The pretend option can be used for "simulating" the migration and grabbing - // the SQL queries that would fire if the migration were to be run against - // a database for real, which is helpful for double checking migrations. - $pretend = $this->input->getOption('pretend'); - - $path = $this->getMigrationPath(); - - $this->migrator->setConnection($this->input->getOption('database')); - $this->migrator->run($path, ['pretend' => $pretend]); - - // Once the migrator has run we will grab the note output and send it out to - // the console screen, since the migrator itself functions without having - // any instances of the OutputInterface contract passed into the class. - foreach ($this->migrator->getNotes() as $note) { - $this->output->writeln($note); - } - - // Finally, if the "seed" option has been given, we will re-run the database - // seed task to re-populate the database, which is convenient when adding - // a migration and a seed at the same time, as it is only this command. - if ($this->input->getOption('seed')) { - $this->call('db:seed', ['--force' => true]); - } - } - - /** - * {@inheritDoc} - */ - protected function getOptions() - { - return array( - array('bench', null, InputOption::VALUE_OPTIONAL, 'The name of the workbench to migrate.', null), - - array('database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'), - - array('force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'), - - array('path', null, InputOption::VALUE_OPTIONAL, 'The path to migration files.', null), - - array('package', null, InputOption::VALUE_OPTIONAL, 'The package to migrate.', null), - - array('pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run.'), - - array('seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run.'), - ); - } -} diff --git a/src/Console/Migrations/MigrateMakeCommand.php b/src/Console/Migrations/MigrateMakeCommand.php deleted file mode 100644 index 6a83fbfd..00000000 --- a/src/Console/Migrations/MigrateMakeCommand.php +++ /dev/null @@ -1,123 +0,0 @@ -creator = $creator; - $this->packagePath = $packagePath; - $this->composer = $composer; - } - - /** - * {@inheritDoc} - */ - public function fire() - { - // It's possible for the developer to specify the tables to modify in this - // schema operation. The developer may also specify if this label needs - // to be freshly created so we can create the appropriate migrations. - $name = $this->input->getArgument('name'); - - $label = $this->input->getOption('label'); - - $modify = $this->input->getOption('create'); - - if (!$label && is_string($modify)) { - $label = $modify; - } - - // Now we are ready to write the migration out to disk. Once we've written - // the migration out, we will dump-autoload for the entire framework to - // make sure that the migrations are registered by the class loaders. - $this->writeMigration($name, $label); - - $this->composer->dumpAutoloads(); - } - - /** - * Write the migration file to disk. - * - * @param string $name - * @param string $label - * @param bool $create - * - * @return string - */ - protected function writeMigration($name, $label) - { - $path = $this->getMigrationPath(); - - $file = pathinfo($this->creator->create($name, $path, $label), PATHINFO_FILENAME); - - $this->line("Created Migration: $file"); - } - - /** - * {@inheritDoc} - */ - protected function getArguments() - { - return array( - array('name', InputArgument::REQUIRED, 'The name of the migration'), - ); - } - - /** - * {@inheritDoc} - */ - protected function getOptions() - { - return array( - array('bench', null, InputOption::VALUE_OPTIONAL, 'The workbench the migration belongs to.', null), - - array('create', null, InputOption::VALUE_OPTIONAL, 'The label schema to be created.'), - - array('package', null, InputOption::VALUE_OPTIONAL, 'The package the migration belongs to.', null), - - array('path', null, InputOption::VALUE_OPTIONAL, 'Where to store the migration.', null), - - array('label', null, InputOption::VALUE_OPTIONAL, 'The label to migrate.'), - ); - } -} diff --git a/src/Console/Migrations/MigrateRefreshCommand.php b/src/Console/Migrations/MigrateRefreshCommand.php deleted file mode 100644 index 2be597bb..00000000 --- a/src/Console/Migrations/MigrateRefreshCommand.php +++ /dev/null @@ -1,89 +0,0 @@ -confirmToProceed()) { - return; - } - - $database = $this->input->getOption('database'); - - $force = $this->input->getOption('force'); - - $this->call('migrate:reset', array( - '--database' => $database, '--force' => $force, - )); - - // The refresh command is essentially just a brief aggregate of a few other of - // the migration commands and just provides a convenient wrapper to execute - // them in succession. We'll also see if we need to re-seed the database. - $this->call('migrate', array( - '--database' => $database, '--force' => $force, - )); - - if ($this->needsSeeding()) { - $this->runSeeder($database); - } - } - - /** - * Determine if the developer has requested database seeding. - * - * @return bool - */ - protected function needsSeeding() - { - return $this->option('seed') || $this->option('seeder'); - } - - /** - * Run the database seeder command. - * - * @param string $database - */ - protected function runSeeder($database) - { - $class = $this->option('seeder') ?: 'DatabaseSeeder'; - - $this->call('db:seed', array('--database' => $database, '--class' => $class)); - } - - /** - * {@inheritDoc} - */ - protected function getOptions() - { - return array( - array('database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'), - - array('force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'), - - array('seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run.'), - - array('seeder', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder.'), - ); - } -} diff --git a/src/Console/Migrations/MigrateResetCommand.php b/src/Console/Migrations/MigrateResetCommand.php deleted file mode 100644 index a434f05b..00000000 --- a/src/Console/Migrations/MigrateResetCommand.php +++ /dev/null @@ -1,83 +0,0 @@ -migrator = $migrator; - } - - /** - * {@inheritDoc} - */ - public function fire() - { - if (!$this->confirmToProceed()) { - return; - } - - $this->migrator->setConnection($this->input->getOption('database')); - - $pretend = $this->input->getOption('pretend'); - - while (true) { - $count = $this->migrator->rollback(['pretend' => $pretend]); - - // Once the migrator has run we will grab the note output and send it out to - // the console screen, since the migrator itself functions without having - // any instances of the OutputInterface contract passed into the class. - foreach ($this->migrator->getNotes() as $note) { - $this->output->writeln($note); - } - - if ($count == 0) { - break; - } - } - } - - /** - * {@inheritDoc} - */ - protected function getOptions() - { - return array( - array('database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'), - - array('force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'), - - array('pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run.'), - ); - } -} diff --git a/src/Console/Migrations/MigrateRollbackCommand.php b/src/Console/Migrations/MigrateRollbackCommand.php deleted file mode 100644 index 9deb4b75..00000000 --- a/src/Console/Migrations/MigrateRollbackCommand.php +++ /dev/null @@ -1,79 +0,0 @@ -migrator = $migrator; - } - - /** - * {@inheritDoc} - */ - public function fire() - { - if (!$this->confirmToProceed()) { - return; - } - - $this->migrator->setConnection($this->input->getOption('database')); - - $pretend = $this->input->getOption('pretend'); - - $this->migrator->rollback(['pretend' => $pretend]); - - // Once the migrator has run we will grab the note output and send it out to - // the console screen, since the migrator itself functions without having - // any instances of the OutputInterface contract passed into the class. - foreach ($this->migrator->getNotes() as $note) { - $this->output->writeln($note); - } - } - - /** - * {@inheritDoc} - */ - protected function getOptions() - { - return array( - array('database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'), - - array('force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'), - - array('pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run.'), - ); - } -} diff --git a/src/Eloquent/Builder.php b/src/Eloquent/Builder.php deleted file mode 100644 index 9881a117..00000000 --- a/src/Eloquent/Builder.php +++ /dev/null @@ -1,1987 +0,0 @@ -query = $query; - } - - /** - * Find a model by its primary key. - * - * @param mixed $id - * @param mixed $properties - * - * @return Model|static|null|Collection - */ - public function find($id, $properties = ['*']) - { - // If the dev did not specify the $id as an int it would break - // so we cast it anyways. - - if (is_array($id)) { - return $this->findMany(array_map('intval', $id), $properties); - } - - if ($this->model->getKeyName() === 'id') { - // ids are treated differently in neo4j so we have to adapt the query to them. - $this->query->where($this->model->getKeyName().'('.$this->query->modelAsNode().')', '=', (int) $id); - } else { - $this->query->where($this->model->getKeyName(), '=', $id); - } - - return $this->first($properties); - } - - /** - * Find a model by its primary key. - * - * @param array $ids - * @param array $columns - * - * @return Collection - */ - public function findMany($ids, $columns = ['*']) - { - if (empty($ids)) { - return $this->model->newCollection(); - } - - $this->query->whereIn($this->model->getQualifiedKeyName(), $ids); - - return $this->get($columns); - } - - /** - * Find a model by its primary key or throw an exception. - * - * @param mixed $id - * @param array $columns - * - * @return Model|Collection - * - * @throws ModelNotFoundException - */ - public function findOrFail($id, $columns = ['*']) - { - $result = $this->find($id, $columns); - - if (is_array($id)) { - if (count($result) === count(array_unique($id))) { - return $result; - } - } elseif (!is_null($result)) { - return $result; - } - - throw (new ModelNotFoundException())->setModel(get_class($this->model)); - } - - /** - * Execute the query and get the first result. - * - * @param array $columns - * - * @return Model|static|null - */ - public function first($columns = ['*']) - { - return $this->take(1)->get($columns)->first(); - } - - /** - * Execute the query and get the first result or throw an exception. - * - * @param array $columns - * - * @return Model|static - * - * @throws ModelNotFoundException - */ - public function firstOrFail($columns = ['*']) - { - if (!is_null($model = $this->first($columns))) { - return $model; - } - - throw (new ModelNotFoundException())->setModel(get_class($this->model)); - } - - /** - * Execute the query as a "select" statement. - * - * @param array $columns - * - * @return Collection|static[] - */ - public function get($columns = ['*']) - { - $models = $this->getModels($columns); - - // If we actually found models we will also eager load any relationships that - // have been specified as needing to be eager loaded, which will solve the - // n+1 query issue for the developers to avoid running a lot of queries. - if (count($models) > 0) { - $models = $this->eagerLoadRelations($models); - } - - return $this->model->newCollection($models); - } - - /** - * Get a single column's value from the first result of a query. - * - * @param string $column - * - * @return mixed - */ - public function value($column) - { - $result = $this->first([$column]); - - if ($result) { - return $result->{$column}; - } - - return null; - } - - /** - * Get a single column's value from the first result of a query. - * - * This is an alias for the "value" method. - * - * @param string $column - * - * @return mixed - * - * @deprecated since version 5.1. - */ - public function pluck($column) - { - return $this->value($column); - } - - /** - * Chunk the results of the query. - * - * @param int $count - * @param callable $callback - */ - public function chunk($count, callable $callback) - { - $results = $this->forPage($page = 1, $count)->get(); - - while (count($results) > 0) { - // On each chunk result set, we will pass them to the callback and then let the - // developer take care of everything within the callback, which allows us to - // keep the memory low for spinning through large result sets for working. - if (call_user_func($callback, $results) === false) { - break; - } - - ++$page; - - $results = $this->forPage($page, $count)->get(); - } - } - - /** - * Get an array with the values of a given column. - * - * @param string $column - * @param string $key - * - * @return \Illuminate\Support\Collection - */ - public function lists($column, $key = null) - { - $results = $this->query->lists($column, $key); - - // If the model has a mutator for the requested column, we will spin through - // the results and mutate the values so that the mutated version of these - // columns are returned as you would expect from these Eloquent models. - if ($this->model->hasGetMutator($column)) { - foreach ($results as &$value) { - $fill = [$column => $value]; - - $value = $this->model->newFromBuilder($fill)->$column; - } - } - - return new Collection($results); - } - - /** - * Increment a column's value by a given amount. - * - * @param string $column - * @param int $amount - * @param array $extra - * - * @return int - */ - public function increment($column, $amount = 1, array $extra = []) - { - $extra = $this->addUpdatedAtColumn($extra); - - return $this->query->increment($column, $amount, $extra); - } - - /** - * Decrement a column's value by a given amount. - * - * @param string $column - * @param int $amount - * @param array $extra - * - * @return int - */ - public function decrement($column, $amount = 1, array $extra = []) - { - $extra = $this->addUpdatedAtColumn($extra); - - return $this->query->decrement($column, $amount, $extra); - } - - /** - * Add the "updated at" column to an array of values. - * - * @param array $values - * - * @return array - */ - protected function addUpdatedAtColumn(array $values) - { - if (!$this->model->usesTimestamps()) { - return $values; - } - - $column = $this->model->getUpdatedAtColumn(); - - return Arr::add($values, $column, $this->model->freshTimestampString()); - } - - /** - * Delete a record from the database. - * - * @return mixed - */ - public function delete() - { - if (isset($this->onDelete)) { - return call_user_func($this->onDelete, $this); - } - - return $this->query->delete(); - } - - /** - * Run the default delete function on the builder. - * - * @return mixed - */ - public function forceDelete() - { - return $this->query->delete(); - } - - /** - * Register a replacement for the default delete function. - * - * @param Closure $callback - */ - public function onDelete(Closure $callback) - { - $this->onDelete = $callback; - } - - /** - * Declare identifiers to carry over to the next part of the query. - * - * @param array $parts Should be associative of the form ['value' => 'identifier'] - * and will be mapped to 'WITH value as identifier' - * - * @return Builder|static - */ - public function carry(array $parts) - { - $this->query->with($parts); - - return $this; - } - - /** - * Get the hydrated models without eager loading. - * - * @param array $properties - * - * @return array|static[] - */ - public function getModels($properties = array('*')) - { - // First, we will simply get the raw results from the query builders which we - // can use to populate an array with Eloquent models. We will pass columns - // that should be selected as well, which are typically just everything. - $results = $this->query->get($properties); - - $models = $this->resultsToModels($this->model->getConnectionName(), $results); - // hold the unique results (discarding duplicates resulting from the query) - - // $unique = []; - - // FIXME: when we detect relationships, we need to remove duplicate - // records returned by query. - - // $index = 0; - // if (!empty($this->mutations)) { - // foreach ($results->getRelationships() as $relationship) { - // $unique[] = $models[$index]; - // ++$index; - // } - - // $models = $unique; - // } - - // Once we have the results, we can spin through them and instantiate a fresh - // model instance for each records we retrieved from the database. We will - // also set the proper connection name for the model after we create it. - return $models; - } - - /** - * Eager load the relationships for the models. - * - * @param array $models - * - * @return array - */ - public function eagerLoadRelations(array $models) - { - foreach ($this->eagerLoad as $name => $constraints) { - // For nested eager loads we'll skip loading them here and they will be set as an - // eager load on the query to retrieve the relation so that they will be eager - // loaded on that query, because that is where they get hydrated as models. - if (strpos($name, '.') === false) { - $models = $this->loadRelation($models, $name, $constraints); - } - } - - return $models; - } - - /** - * Eagerly load the relationship on a set of models. - * - * @param array $models - * @param string $name - * @param Closure $constraints - * - * @return array - */ - protected function loadRelation(array $models, $name, Closure $constraints) - { - // First we will "back up" the existing where conditions on the query so we can - // add our eager constraints. Then we will merge the wheres that were on the - // query back to it in order that any where conditions might be specified - // to be taken into consideration with the query. - $relation = $this->getRelation($name); - - // First we will check for existing relationships in models - // if that exists then we'll have to take out the end models - // from the relationships - this happens in the case of - // nested relations. - // if ($this->hasRelationships($models)) { - // $models = array_map(function($model) { - // return $model->getEndModel(); - // }, $models); - // } - - $relation->addEagerConstraints($models); - - call_user_func($constraints, $relation); - - $models = $relation->initRelation($models, $name); - - // Once we have the results, we just match those back up to their parent models - // using the relationship instance. Then we just return the finished arrays - // of models which have been eagerly hydrated and are readied for return. - $results = $relation->getEager(); - - return $relation->match($models, $results, $name); - } - - /** - * Determines whether the given array includes instances - * of \Vinelab\NeoEloquent\Eloquent\Relationship. - * - * @param array $models - * - * @return bool - */ - protected function hasRelationships(array $models) - { - $itDoes = false; - - foreach ($models as $model) { - if ($model instanceof EloquentRelationship) { - $itDoes = true; - break; - } - } - - return $itDoes; - } - - /** - * Get the relation instance for the given relation name. - * - * @param string $relation - * - * @return Relation - */ - public function getRelation($relation) - { - // We want to run a relationship query without any constrains so that we will - // not have to remove these where clauses manually which gets really hacky - // and is error prone while we remove the developer's own where clauses. - $query = Relation::noConstraints(function () use ($relation) { - return $this->getModel()->$relation(); - }); - - $nested = $this->nestedRelations($relation); - - // If there are nested relationships set on the query, we will put those onto - // the query instances so that they can be handled after this relationship - // is loaded. In this way they will all trickle down as they are loaded. - if (count($nested) > 0) { - $query->getQuery()->with($nested); - } - - return $query; - } - - /** - * Get the deeply nested relations for a given top-level relation. - * - * @param string $relation - * - * @return array - */ - protected function nestedRelations($relation) - { - $nested = []; - - // We are basically looking for any relationships that are nested deeper than - // the given top-level relationship. We will just check for any relations - // that start with the given top relations and adds them to our arrays. - foreach ($this->eagerLoad as $name => $constraints) { - if ($this->isNested($name, $relation)) { - $nested[substr($name, strlen($relation.'.'))] = $constraints; - } - } - - return $nested; - } - - /** - * Determine if the relationship is nested. - * - * @param string $name - * @param string $relation - * - * @return bool - */ - protected function isNested($name, $relation) - { - $dots = Str::contains($name, '.'); - - return $dots && Str::startsWith($name, $relation.'.'); - } - - /** - * Add a basic where clause to the query. - * - * @param string $column - * @param string $operator - * @param mixed $value - * @param string $boolean - * - * @return $this - */ - public function where($column, $operator = null, $value = null, $boolean = 'and') - { - if ($column instanceof Closure) { - $query = $this->model->newQueryWithoutScopes(); - - call_user_func($column, $query); - - $this->query->addNestedWhereQuery($query->getQuery(), $boolean); - } else { - call_user_func_array([$this->query, 'where'], func_get_args()); - } - - return $this; - } - - /** - * Add an "or where" clause to the query. - * - * @param string $column - * @param string $operator - * @param mixed $value - * - * @return Builder|static - */ - public function orWhere($column, $operator = null, $value = null) - { - return $this->where($column, $operator, $value, 'or'); - } - - /** - * Turn Neo4j result set into the corresponding model. - * - * @param string $connection - * @param ?CypherList $results - * - * @return array - */ - protected function resultsToModels($connection, ?CypherList $results = null) - { - $models = []; - - $results = $results ?? new CypherList(); - - if ($results) { - $resultsByIdentifier = $this->getRecordsByPlaceholders($results); - $relationships = $this->getRelationshipRecords($results); - - if (!empty($relationships) && !empty($this->mutations)) { - $startIdentifier = $this->getStartNodeIdentifier($resultsByIdentifier, $relationships); - $endIdentifier = $this->getEndNodeIdentifier($resultsByIdentifier, $relationships); - - foreach ($relationships as $index => $resultRelationship) { - $startModelClass = $this->getMutationModel($startIdentifier); - $endModelClass = $this->getMutationModel($endIdentifier); - - if ($this->shouldMutate($endIdentifier) && $this->isMorphMutation($endIdentifier)) { - $models[] = $this->mutateToOrigin($results, $resultsByIdentifier); - } else { - $startNode = (is_array($resultsByIdentifier[$startIdentifier])) ? $resultsByIdentifier[$startIdentifier][$index] : reset($resultsByIdentifier[$startIdentifier]); - $endNode = (is_array($resultsByIdentifier[$endIdentifier])) ? $resultsByIdentifier[$endIdentifier][$index] : reset($resultsByIdentifier[$endIdentifier]); - $models[] = [ - $startIdentifier => $this->newModelFromNode($startNode, $startModelClass, $connection), - $endIdentifier => $this->newModelFromNode($endNode, $endModelClass, $connection), - ]; - } - } - } else { - foreach ($resultsByIdentifier as $identifier => $nodes) { - if ($this->shouldMutate($identifier)) { - $models[] = $this->mutateToOrigin($results, $resultsByIdentifier); - } else { - foreach ($nodes as $result) { - if ($result instanceof Node) { - $model = $this->newModelFromNode($result, $this->model, $connection); - $models[] = $model; - } - } - } - } - } - } - - return $models; - } - - protected function getStartNodeIdentifier($resultsByIdentifier, $relationships) - { - return $this->getNodeIdentifier($resultsByIdentifier, $relationships, 'start'); - } - - protected function getEndNodeIdentifier($resultsByIdentifier, $relationships) - { - return $this->getNodeIdentifier($resultsByIdentifier, $relationships, 'end'); - } - - protected function getNodeIdentifier($resultsByIdentifier, $relationships, $type = 'start') - { - $method = 'getStartNodeId'; - - if ($type === 'end') { - $method = 'getEndNodeId'; - } - - $relationship = reset($relationships); - - foreach ($resultsByIdentifier as $identifier => $nodes) { - foreach ($nodes as $node) { - if ($node->getId() === $relationship->$method()) { - return $identifier; - } - } - } - } - - /** - * Get a Model instance out of the given node. - * - * @param Node $node - * @param Model $model - * @param string $connection - * - * @return Model - */ - public function newModelFromNode(Node $node, Model $model, $connection = null) - { - // let's begin with a proper connection - if (!$connection) { - $connection = $model->getConnectionName(); - } - - // get the attributes ready - $attributes = array_merge($node->getProperties()->toArray(), $model->getAttributes()); - - // we will check to see whether we should use Neo4j's built-in ID. - if ($model->getKeyName() === 'id') { - $attributes['id'] = $node->getId(); - } - - // This is a regular record that we should deal with the normal way, creating an instance - // of the model out of the fetched attributes. - $fresh = $model->newFromBuilder($attributes); - $fresh->setConnection($connection); - - return $fresh; - } - - /** - * Turn Neo4j result set into the corresponding model with its relations. - * - * @param string $connection - * @param CypherList $results - * - * @return array - */ - protected function resultsToModelsWithRelations($connection, CypherList $results) - { - $models = []; - - if (!$results->isEmpty()) { - $grammar = $this->getQuery()->getGrammar(); - -// $nodesByIdentifier = $results->getAllByIdentifier(); -// -// foreach ($nodesByIdentifier as $identifier => $nodes) { -// // Now that we have the attributes, we first check for mutations -// // and if exists, we will need to mutate the attributes accordingly. -// if ($this->shouldMutate($identifier)) { -// foreach ($nodes as $node) { -// $attributes = $node->getProperties(); -// $cropped = $grammar->cropLabelIdentifier($identifier); -// -// if (!isset($models[$cropped])) { -// $models[$cropped] = []; -// } -// -// if (isset($this->mutations[$cropped])) { -// $mutationModel = $this->getMutationModel($cropped); -// $models[$cropped][] = $this->newModelFromNode($node, $mutationModel); -// } -// } -// } -// } - - $recordsByPlaceholders = $this->getRecordsByPlaceholders($results); - - foreach ($recordsByPlaceholders as $placeholder => $records) { - - // Now that we have the attributes, we first check for mutations - // and if exists, we will need to mutate the attributes accordingly. - if ($this->shouldMutate($placeholder)) { - $cropped = $grammar->cropLabelIdentifier($placeholder); -// $attributes = $record->values(); - - foreach ($records as $record) { - if (!isset($models[$cropped])) { - $models[$cropped] = []; - } - - if (isset($this->mutations[$cropped])) { - $mutationModel = $this->getMutationModel($cropped); - $models[$cropped][] = $this->newModelFromNode($record, $mutationModel); - } - } - } - } - } - - return $models; - } - - /** - * Mutate a result back into its original Model. - * - * @param mixed $result - * @param array $attributes - * - * @return array - */ - public function mutateToOrigin($result, $attributes) - { - $mutations = []; - - // Transform mutations back to their origin - foreach ($attributes as $mutation => $values) { - // First we should see whether this mutation can be resolved so that - // we take it into consideration otherwise we skip to the next iteration. - if (!$this->resolvableMutation($mutation)) { - continue; - } - // Since this mutation should be resolved by us then we check whether it is - // a Many or One mutation. - if ($this->isManyMutation($mutation)) { - $mutations = $this->mutateManyToOrigin($attributes); - } - // Dealing with Morphing relations requires that we determine the morph_type out of the relationship - // and mutating back to that class. - elseif ($this->isMorphMutation($mutation)) { - $mutant = $this->mutateMorphToOrigin($result, $attributes); - - if ($this->getMutation($mutation)['type'] == 'morphEager') { - $mutations[$mutation] = $mutant; - } else { - $mutations = reset($mutant); - } - } - // Dealing with One mutations is simply returning an associative array with the mutation - // label being the $key and the related model is $value. - else { - $node = current($values); - $mutations[$mutation] = $this->newModelFromNode($node, $this->getMutationModel($mutation)); - } - } - - return $mutations; - } - - /** - * In the case of Many mutations we need to return an associative array having both - * relations as a single record so that when we match them back we know which result - * belongs to which parent node. - * - * @param array $attributes - */ - public function mutateManyToOrigin($results) - { - $mutations = []; - - foreach ($this->getMutations() as $label => $info) { - $mutationModel = $this->getMutationModel($label); - $mutations[$label] = $this->newModelFromNode(current($results[$label]), $mutationModel); - } - - return $mutations; - } - - protected function mutateMorphToOrigin($result, $attributesByLabel) - { - $mutations = []; - - foreach ($this->getMorphMutations() as $label => $info) { - // Let's see where we should be getting the morph Class name from. - $mutationModelProperty = $this->getMutationModel($label); - // We need the relationship from the result since it has the mutation model property's - // value being the model that we should mutate to as set earlier by a HyperEdge. - // NOTE: 'r' is statically set in CypherGrammer to represent the relationship. - // Now we have an \Everyman\Neo4j\Relationship instance that has our morph class name. - /** @var \Laudis\Neo4j\Types\Relationship $relationship */ - $relationship = current($this->getRelationshipRecords($result)); - // Get the morph class name. - $class = $relationship->getProperties()->get($mutationModelProperty); - // we need the model attributes though we might receive a nested - // array that includes them on level 2 so we check - // whether what we have is the array of attrs - if (!Helpers::isAssocArray($attributesByLabel[$label])) { - $attributes = current($attributesByLabel[$label]); - if ($attributes instanceof Node) { - $attributes = $this->getNodeAttributes($attributes); - } - } else { - $attributes = $attributesByLabel[$label]; - } - // Create a new instance of it from builder. - $model = (new $class())->newFromBuilder($attributes); - // And that my friend, is our mutations model =) - $mutations[] = $model; - } - - return $mutations; - } - - /** - * Determine whether attributes are mutations - * and should be transformed back. It is considered - * a mutation only when the attributes' keys - * and mutations keys match. - * - * @param array $attributes - * - * @return bool - */ - public function shouldMutate($identifier) - { - $grammar = $this->getQuery()->getGrammar(); - $identifier = $grammar->cropLabelIdentifier($identifier); - $mutations = array_keys($this->mutations); - - return in_array($identifier, $mutations); - } - - /** - * Get the properties (attribtues in Eloquent terms) - * out of a result row. - * - * @param array $columns The columns retrieved by the result - * @param Row $row - * @param array $columns - * - * @return array - * - * @deprecated 2.0 using getNodeAttributes instead - */ - public function getProperties(array $resultColumns, Row $row) - { - dd('Get Properties, Everyman dependent'); - $attributes = array(); - - $columns = $this->query->columns; - - // What we get returned from the client is a result set - // and each result is either a Node or a single column value - // so we first extract the returned value and retrieve - // the attributes according to the result type. - - // Only when requesting a single property - // will we extract the current() row of result. - - $current = $row->current(); - - $result = ($current instanceof Node) ? $current : $row; - - if ($this->isRelationship($resultColumns)) { - // You must have chosen certain properties (columns) to be returned - // which means that we should map the values to their corresponding keys. - foreach ($resultColumns as $key => $property) { - $value = $row[$property]; - - if ($value instanceof Node) { - $value = $this->getNodeAttributes($value); - } else { - // Our property should be extracted from the query columns - // instead of the result columns - $property = $columns[$key]; - - // as already assigned, RETURNed props will be preceded by an 'n.' - // representing the node we're targeting. - $returned = $this->query->modelAsNode().".{$property}"; - - $value = $row[$returned]; - } - - $attributes[$property] = $value; - } - - // If the node id is in the columns we need to treat it differently - // since Neo4j's convenience with node ids will be retrieved as id(n) - // instead of n.id. - - // WARNING: Do this after setting all the attributes to avoid overriding it - // with a null value or colliding it with something else, some Daenerys dragons maybe ?! - if (!is_null($columns) && in_array('id', $columns)) { - $attributes['id'] = $row['id('.$this->query->modelAsNode().')']; - } - } elseif ($result instanceof Node) { - $attributes = $this->getNodeAttributes($result); - } elseif ($result instanceof Row) { - $attributes = $this->getRowAttributes($result, $columns, $resultColumns); - } - - return $attributes; - } - - /** - * Gather the properties of a Node including its id. - * - * @return array - */ - public function getNodeAttributes(Node $node) - { - // Extract the properties of the node - $attributes = $node->getProperties()->toArray(); - - // Add the node id to the attributes since \Everyman\Neo4j\Node - // does not consider it to be a property, it is treated differently - // and available through the getId() method. - $attributes['id'] = $node->getId(); - - return $attributes; - } - - /** - * Get the attributes of a result Row. - * - * @param Row $row - * @param array $columns The query columns - * @param array $resultColumns The result columns that can be extracted from a \Everyman\Neo4j\Query\ResultSet - * - * @return array - */ - public function getRowAttributes(Row $row, $columns, $resultColumns) - { - $attributes = []; - - foreach ($resultColumns as $key => $column) { - $attributes[$columns[$key]] = $row[$column]; - } - - return $attributes; - } - - /** - * Add an INCOMING "<-" relationship MATCH to the query. - * - * @param Vinelab\NeoEloquent\Eloquent\Model $parent The parent model - * @param Vinelab\NeoEloquent\Eloquent\Model $related The related model - * @param string $relationship - * - * @return Vinelab\NeoEloquent\Eloquent|static - */ - public function matchIn($parent, $related, $relatedNode, $relationship, $property, $value = null, $boolean = 'and') - { - // Add a MATCH clause for a relation to the query - $this->query->matchRelation($parent, $related, $relatedNode, $relationship, $property, $value, 'in', $boolean); - - return $this; - } - - /** - * Add an OUTGOING "->" relationship MATCH to the query. - * - * @param Vinelab\NeoEloquent\Eloquent\Model $parent The parent model - * @param Vinelab\NeoEloquent\Eloquent\Model $related The related model - * @param string $relationship - * - * @return Vinelab\NeoEloquent\Eloquent|static - */ - public function matchOut($parent, $related, $relatedNode, $relationship, $property, $value = null, $boolean = 'and') - { - $this->query->matchRelation($parent, $related, $relatedNode, $relationship, $property, $value, 'out', $boolean); - - return $this; - } - - /** - * Add an outgoing morph relationship to the query, - * a morph relationship usually ignores the end node type since it doesn't know - * what it would be so we'll only set the start node and hope to get it right when we match it. - * - * @param Vinelab\NeoEloquent\Eloquent\Model $parent - * @param string $relatedNode - * @param string $property - * @param mixed $value - * - * @return Vinelab\NeoEloquent\Eloquent|static - */ - public function matchMorphOut($parent, $relatedNode, $property, $value = null, $boolean = 'and') - { - $this->query->matchMorphRelation($parent, $relatedNode, $property, $value, $boolean); - - return $this; - } - - /** - * Paginate the given query. - * - * @param int $perPage - * @param array $columns - * @param string $pageName - * @param int|null $page - * - * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator - * - * @throws InvalidArgumentException - */ - public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) - { - $total = $this->query->getCountForPagination(); - - $this->query->forPage( - $page = $page ?: Paginator::resolveCurrentPage($pageName), - $perPage = $perPage ?: $this->model->getPerPage() - ); - - return new LengthAwarePaginator($this->get($columns), $total, $perPage, $page, [ - 'path' => Paginator::resolveCurrentPath(), - 'pageName' => $pageName, - ]); - } - - /** - * Update a record in the database. - * - * @param array $values - * - * @return int - */ - public function update(array $values) - { - return $this->query->update($this->addUpdatedAtColumn($values)); - } - - /** - * Get a paginator only supporting simple next and previous links. - * - * This is more efficient on larger data-sets, etc. - * - * @param int $perPage - * @param array $columns - * @param string $pageName - * - * @return Paginator - * - * @internal param \Illuminate\Pagination\Factory $paginator - */ - public function simplePaginate($perPage = null, $columns = array('*'), $pageName = 'page') - { - $paginator = $this->query->getConnection()->getPaginator(); - $page = $paginator->getCurrentPage(); - $perPage = $perPage ?: $this->model->getPerPage(); - $this->query->skip(($page - 1) * $perPage)->take($perPage + 1); - - return new Paginator($this->get($columns), $perPage, $page, [ - 'path' => Paginator::resolveCurrentPath(), - 'pageName' => $pageName, - ]); - } - - /** - * Add a mutation to the query. - * - * @param string $holder - * @param Model|string $model String in the case of morphs where we do not know - * the morph model class name - */ - public function addMutation($holder, $model, $type = 'one') - { - $this->mutations[$holder] = [ - 'type' => $type, - 'model' => $model, - ]; - } - - /** - * Add a mutation of the type 'many' to the query. - * - * @param string $holder - * @param Model $model - */ - public function addManyMutation($holder, Model $model) - { - $this->addMutation($holder, $model, 'many'); - } - - /** - * Add a mutation of the type 'morph' to the query. - * - * @param string $holder - * @param string $model - */ - public function addMorphMutation($holder, $model = 'morph_type') - { - return $this->addMutation($holder, $model, 'morph'); - } - - /** - * Add a mutation of the type 'morph' to the query. - * - * @param string $holder - * @param string $model - */ - public function addEagerMorphMutation($holder, $model = 'morph_type') - { - return $this->addMutation($holder, $model, 'morphEager'); - } - - /** - * Determine whether a mutation is of the type 'many'. - * - * @param string $mutation - * - * @return bool - */ - public function isManyMutation($mutation) - { - return isset($this->mutations[$mutation]) && $this->mutations[$mutation]['type'] === 'many'; - } - - /** - * Determine whether this mutation is of the typ 'morph'. - * - * @param string $mutation - * - * @return bool - */ - public function isMorphMutation($mutation) - { - if (!is_array($mutation) && isset($this->mutations[$mutation])) { - $mutation = $this->getMutation($mutation); - } - - return $mutation['type'] === 'morph' || $mutation['type'] === 'morphEager'; - } - - /** - * Get the mutation model. - * - * @param string $mutation - * - * @return Vinelab\NeoEloquent\Eloquent\Model - */ - public function getMutationModel($mutation) - { - if ($this->mutationExists($mutation)) { - return $this->getMutation($mutation)['model']; - } - } - - /** - * Determine whether a mutation of the given type exists. - * - * @param string $mutation - * - * @return bool - */ - public function mutationExists($mutation) - { - return isset($this->mutations[$mutation]); - } - - /** - * Get the mutation type. - * - * @param string $mutation - * - * @return string - */ - public function getMutationType($mutation) - { - return $this->getMutation($mutation)['type']; - } - - /** - * Determine whether a mutation can be resolved - * by simply checking whether it exists in the $mutations. - * - * @param string $mutation - * - * @return bool - */ - public function resolvableMutation($mutation) - { - return isset($this->mutations[$mutation]); - } - - /** - * Get the mutations. - * - * @return array - */ - public function getMutations() - { - return $this->mutations; - } - - /** - * Get a single mutation. - * - * @param string $mutation - * - * @return array - */ - public function getMutation($mutation) - { - return $this->mutations[$mutation]; - } - - /** - * Get the mutations of type 'morph'. - * - * @return array - */ - public function getMorphMutations() - { - return array_filter($this->getMutations(), function ($mutation) { return $this->isMorphMutation($mutation); }); - } - - /** - * Determine whether the intended result is a relationship result between nodes, - * we can tell by the format of the requested properties, in case the requested - * properties were in the form of 'user.name' we are pretty sure it is an attribute - * of a node, otherwise if they're plain strings like 'user' and they're more than one then - * the reference is assumed to be a Node placeholder rather than a property. - * - * @param Row $row - * - * @return bool - */ - public function isRelationship(array $columns) - { - $matched = array_filter($columns, function ($column) { - // As soon as we find that a property does not - // have a dot '.' in it we assume it is a relationship, - // unless it is the id of a node which is where we look - // at a pattern that matches id(any character here). - if (preg_match('/^([a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+)|(id\(.*\))$/', $column)) { - return false; - } - - return true; - }); - - return count($matched) > 1 ? true : false; - } - - /** - * Add a relationship query condition. - * - * @param string $relation - * @param string $operator - * @param int $count - * @param string $boolean - * @param Closure $callback - * - * @return Builder|static - */ - public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null) - { - if (strpos($relation, '.') !== false) { - return $this->hasNested($relation, $operator, $count, $boolean, $callback); - } - - $relation = $this->getHasRelationQuery($relation); - - $query = $relation->getRelated()->newQuery(); - // This will make sure that any query we add here will consider the related - // model as our reference Node. Similar to switching contexts. - $this->getQuery()->from = $query->getModel()->nodeLabel(); - - /* - * In graph we do not need to act on the count of the relationships when dealing - * with a whereHas() since the database will not return the result unless a relationship - * exists between two nodes. - */ - $prefix = $relation->getRelatedNode(); - - if ($callback) { - call_user_func($callback, $query); - $this->query->matches = array_merge($this->query->matches, $query->getQuery()->matches); - $this->query->with = array_merge($this->query->with, $query->getQuery()->with); - $this->carry([$relation->getParentNode(), $relation->getRelatedNode()]); - } else { - /* - * The Cypher we're trying to build here would look like this: - * - * MATCH (post:`Post`)-[r:COMMENT]-(comments:`Comment`) - * WITH count(comments) AS comments_count, post - * WHERE comments_count >= 10 - * RETURN post; - * - * Which is the result of Post::has('comments', '>=', 10)->get(); - */ - $countPart = $prefix.'_count'; - $this->carry([$relation->getParentNode(), "count($prefix)" => $countPart]); - $this->whereCarried($countPart, $operator, $count); - } - - $parentNode = $relation->getParentNode(); - $relatedNode = $relation->getRelatedNode(); - // Tell the query to select our parent node only. - $this->select($parentNode); - // Set the relationship match clause. - $method = $this->getMatchMethodName($relation); - - $this->$method($relation->getParent(), - $relation->getRelated(), - $relatedNode, - $relation->getRelationType(), - $relation->getLocalKey(), - $relation->getParentLocalKeyValue(), - $boolean - ); - - // Prefix all the columns with the relation's node placeholder in the query - // and merge the queries that needs to be merged. - $this->prefixAndMerge($query, $prefix); - - /* - * After that we've done everything we need with the Has() and related we need - * to reset the query for the grammar so that whenever we continu querying we make - * sure that we're using the correct grammar. i.e. - * - * $user->whereHas('roles', function(){})->where('id', $user->id)->first(); - */ - $grammar = $this->getQuery()->getGrammar(); - $grammar->setQuery($this->getQuery()); - $this->getQuery()->from = $this->getModel()->nodeLabel(); - - return $this; - } - - /** - * Add nested relationship count conditions to the query. - * - * @param string $relations - * @param string $operator - * @param int $count - * @param string $boolean - * @param Closure|null $callback - * - * @return Builder|static - */ - protected function hasNested($relations, $operator = '>=', $count = 1, $boolean = 'and', $callback = null) - { - $relations = explode('.', $relations); - - // In order to nest "has", we need to add count relation constraints on the - // callback Closure. We'll do this by simply passing the Closure its own - // reference to itself so it calls itself recursively on each segment. - $closure = function ($q) use (&$closure, &$relations, $operator, $count, $boolean, $callback) { - if (count($relations) > 1) { - $q->whereHas(array_shift($relations), $closure); - } else { - $q->has(array_shift($relations), $operator, $count, 'and', $callback); - } - }; - - return $this->has(array_shift($relations), '>=', 1, $boolean, $closure); - } - - /** - * Add a relationship count condition to the query. - * - * @param string $relation - * @param string $boolean - * @param Closure|null $callback - * - * @return Builder|static - */ - public function doesntHave($relation, $boolean = 'and', Closure $callback = null) - { - return $this->has($relation, '<', 1, $boolean, $callback); - } - - /** - * Add a relationship count condition to the query with where clauses. - * - * @param string $relation - * @param Closure $callback - * @param string $operator - * @param int $count - * - * @return Builder|static - */ - public function whereHas($relation, Closure $callback, $operator = '>=', $count = 1) - { - return $this->has($relation, $operator, $count, 'and', $callback); - } - - /** - * Add a relationship count condition to the query with an "or". - * - * @param string $relation - * @param string $operator - * @param int $count - * - * @return Builder|static - */ - public function orHas($relation, $operator = '>=', $count = 1) - { - return $this->has($relation, $operator, $count, 'or'); - } - - /** - * Add a relationship count condition to the query with where clauses. - * - * @param string $relation - * @param Closure|null $callback - * - * @return Builder|static - */ - public function whereDoesntHave($relation, Closure $callback = null) - { - return $this->doesntHave($relation, 'and', $callback); - } - - /** - * Add a relationship count condition to the query with where clauses and an "or". - * - * @param string $relation - * @param Closure $callback - * @param string $operator - * @param int $count - * - * @return Builder|static - */ - public function orWhereHas($relation, Closure $callback, $operator = '>=', $count = 1) - { - return $this->has($relation, $operator, $count, 'or', $callback); - } - - /** - * Add the "has" condition where clause to the query. - * - * @param Builder $hasQuery - * @param Relation $relation - * @param string $operator - * @param int $count - * @param string $boolean - * - * @return Builder - */ - protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, $count, $boolean) - { - $this->mergeWheresToHas($hasQuery, $relation); - - if (is_numeric($count)) { - $count = new Expression($count); - } - - return $this->where(new Expression('('.$hasQuery->toCypher().')'), $operator, $count, $boolean); - } - - /** - * Merge the "wheres" from a relation query to a has query. - * - * @param Builder $hasQuery - * @param Relation $relation - */ - protected function mergeWheresToHas(Builder $hasQuery, Relation $relation) - { - // Here we have the "has" query and the original relation. We need to copy over any - // where clauses the developer may have put in the relationship function over to - // the has query, and then copy the bindings from the "has" query to the main. - $relationQuery = $relation->getBaseQuery(); - - $hasQuery = $hasQuery->getModel()->removeGlobalScopes($hasQuery); - - $hasQuery->mergeWheres( - $relationQuery->wheres, $relationQuery->getBindings() - ); - - $this->query->mergeBindings($hasQuery->getQuery()); - } - - /** - * Get the "has relation" base query instance. - * - * @param string $relation - * - * @return Builder - */ - protected function getHasRelationQuery($relation) - { - return Relation::noConstraints(function () use ($relation) { - return $this->getModel()->$relation(); - }); - } - - /** - * Set the relationships that should be eager loaded. - * - * @param mixed $relations - * - * @return $this - */ - public function with($relations) - { - if (is_string($relations)) { - $relations = func_get_args(); - } - - $eagers = $this->parseRelations($relations); - - $this->eagerLoad = array_merge($this->eagerLoad, $eagers); - - return $this; - } - - /** - * Parse a list of relations into individuals. - * - * @param array $relations - * - * @return array - */ - protected function parseRelations(array $relations) - { - $results = []; - - foreach ($relations as $name => $constraints) { - // If the "relation" value is actually a numeric key, we can assume that no - // constraints have been specified for the eager load and we'll just put - // an empty Closure with the loader so that we can treat all the same. - if (is_numeric($name)) { - $f = function () {}; - - list($name, $constraints) = [$constraints, $f]; - } - - // We need to separate out any nested includes. Which allows the developers - // to load deep relationships using "dots" without stating each level of - // the relationship with its own key in the array of eager load names. - $results = $this->parseNested($name, $results); - - $results[$name] = $constraints; - } - - return $results; - } - - /** - * Parse the nested relationships in a relation. - * - * @param string $name - * @param array $results - * - * @return array - */ - protected function parseNested($name, $results) - { - $progress = []; - - // If the relation has already been set on the result array, we will not set it - // again, since that would override any constraints that were already placed - // on the relationships. We will only set the ones that are not specified. - foreach (explode('.', $name) as $segment) { - $progress[] = $segment; - - if (!isset($results[$last = implode('.', $progress)])) { - $results[$last] = function () {}; - } - } - - return $results; - } - - /** - * Call the given model scope on the underlying model. - * - * @param string $scope - * @param array $parameters - * - * @return QueryBuilder - */ - protected function callScope($scope, $parameters) - { - array_unshift($parameters, $this); - - return call_user_func_array([$this->model, $scope], $parameters) ?: $this; - } - - /** - * Get the underlying query builder instance. - * - * @return QueryBuilder|static - */ - public function getQuery() - { - return $this->query; - } - - /** - * Set the underlying query builder instance. - * - * @param QueryBuilder $query - * - * @return $this - */ - public function setQuery($query) - { - $this->query = $query; - - return $this; - } - - /** - * Get the relationships being eagerly loaded. - * - * @return array - */ - public function getEagerLoads() - { - return $this->eagerLoad; - } - - /** - * Set the relationships being eagerly loaded. - * - * @param array $eagerLoad - * - * @return $this - */ - public function setEagerLoads(array $eagerLoad) - { - $this->eagerLoad = $eagerLoad; - - return $this; - } - - /** - * Get the model instance being queried. - * - * @return Model - */ - public function getModel() - { - return $this->model; - } - - /** - * Set a model instance for the model being queried. - * - * @param Model $model - * - * @return $this - */ - public function setModel(Model $model) - { - $this->model = $model; - - $this->query->from($model->nodeLabel()); - - return $this; - } - - /** - * Extend the builder with a given callback. - * - * @param string $name - * @param Closure $callback - */ - public function macro($name, Closure $callback) - { - $this->macros[$name] = $callback; - } - - /** - * Get the given macro by name. - * - * @param string $name - * - * @return Closure - */ - public function getMacro($name) - { - return Arr::get($this->macros, $name); - } - - /** - * Create a new record from the parent Model and new related records with it. - * - * @param array $attributes - * @param array $relations - * - * @return Model - */ - public function createWith(array $attributes, array $relations) - { - // Collect the model attributes and label in the form of ['label' => $label, 'attributes' => $attributes] - // as expected by the Query Builder. - $attributes = $this->prepareForCreation($this->model, $attributes); - $model = ['label' => $this->model->nodeLabel(), 'attributes' => $attributes]; - - /* - * Collect the related models in the following for as expected by the Query Builder: - * - * [ - * 'label' => ['Permission'], - * 'relation' => [ - * 'name' => 'photos', - * 'type' => 'PHOTO', - * 'direction' => 'out', - * ], - * 'values' => [ - * // A mix of models and attributes, doesn't matter really.. - * ['url' => '', 'caption' => ''], - * ['url' => '', 'caption' => ''] - * ] - * ] - */ - $related = []; - foreach ($relations as $relation => $values) { - $name = $relation; - // Get the relation by calling the model's relationship function. - if (!method_exists($this->model, $relation)) { - throw new QueryException("The relation method $relation() does not exist on ".get_class($this->model)); - } - - $relationship = $this->model->$relation(); - // Bring the model from the relationship. - $relatedModel = $relationship->getRelated(); - - // We will first check to see what the dev have passed as values - // so that we make sure that we have an array moving forward - // In the case of a model Id or an associative array or a Model instance it means that - // this is probably a One-To-One relationship or the dev decided not to add - // multiple records as relations so we'll wrap it up in an array. - if ( - (!is_array($values) || Helpers::isAssocArray($values) || $values instanceof Model) - && !($values instanceof Collection) - ) { - $values = [$values]; - } - - $id = $relatedModel->getKeyName(); - $label = $relationship->getRelated()->nodeLabel(); - $direction = $relationship->getEdgeDirection(); - $type = $relationship->getRelationType(); - - // Hold the models that we need to attach - $attach = []; - // Hold the models that we need to create - $create = []; - // Separate the models that needs to be attached from the ones that needs - // to be created. - foreach ($values as $value) { - // If this is a Model then the $exists property will indicate what we need - // so we'll add its id to be attached. - if ($value instanceof Model and $value->exists === true) { - $attach[] = $value->getKey(); - } - // Next we will check whether we got a Collection in so that we deal with it - // accordingly, which guarantees sending an Eloquent result straight in would work. - elseif ($value instanceof Collection) { - $attach = array_merge($attach, $value->lists('id')); - } - // Or in the case where the attributes are neither an array nor a model instance - // then this is assumed to be the model Id that the dev means to attach and since - // Neo4j node Ids are always an int then we take that as a value. - elseif (!is_array($value) && !$value instanceof Model) { - $attach[] = $value; - } - // In this case the record is considered to be new to the market so let's create it. - else { - $create[] = $this->prepareForCreation($relatedModel, $value); - } - } - - $relation = compact('name', 'type', 'direction'); - $related[] = compact('relation', 'label', 'create', 'attach', 'id'); - } - - $results = $this->query->createWith($model, $related); - $models = $this->resultsToModelsWithRelations($this->model->getConnectionName(), $results); - - return (!empty($models)) ? $models : null; - } - - /** - * Prepare model's attributes or instance for creation in a query. - * - * @param string $class - * @param mixed $attributes - * - * @return array - */ - protected function prepareForCreation($class, $attributes) - { - // We need to get the attributes of each $value from $values into - // an instance of the related model so that we make sure that it goes - // through the $fillable filter pipeline. - - // This adds support for having model instances mixed with values, so whenever - // we encounter a Model we take it as our instance - if ($attributes instanceof Model) { - $instance = $attributes; - } - // Reaching here means the dev entered raw attributes (similar to insert()) - // so we'll need to pass the attributes through the model to make sure - // the fillables are respected as expected by the dev. - else { - $instance = new $class($attributes); - } - // Update timestamps on the instance, this will only affect newly - // created models by adding timestamps to them, otherwise it has no effect - // on existing models. - if ($instance->usesTimestamps()) { - $instance->addTimestamps(); - } - - return $instance->toArray(); - } - - /** - * Prefix query bindings and wheres with the relation's model Node placeholder. - * - * @param Builder $query - * @param string $prefix - */ - protected function prefixAndMerge(Builder $query, $prefix) - { - if (is_array($query->getQuery()->wheres)) { - $query->getQuery()->wheres = $this->prefixWheres($query->getQuery()->wheres, $prefix); - } - - $this->query->mergeWheres($query->getQuery()->wheres, $query->getQuery()->getBindings()); - } - - /** - * Prefix where clauses' columns. - * - * @param array $wheres - * @param string $prefix - * - * @return array - */ - protected function prefixWheres(array $wheres, $prefix) - { - return array_map(function ($where) use ($prefix) { - if ($where['type'] == 'Nested') { - $where['query']->wheres = $this->prefixWheres($where['query']->wheres, $prefix); - } else if ($where['type'] != 'Carried' && strpos($where['column'], '.') == false) { - $column = $where['column']; - $where['column'] = ($this->isId($column)) ? $column : $prefix.'.'.$column; - } - - return $where; - }, $wheres); - } - - /** - * Determine whether a value is an Id attribute according to Neo4j. - * - * @param string $value - * - * @return bool - */ - public function isId($value) - { - return preg_match('/^id(\(.*\))?$/', $value); - } - - /** - * Get the match[In|Out] method name out of a relation. - * - * @param * $relation - * - * @return [type] - */ - protected function getMatchMethodName($relation) - { - return 'match'.ucfirst(mb_strtolower($relation->getEdgeDirection())); - } - - /** - * Dynamically handle calls into the query instance. - * - * @param string $method - * @param array $parameters - * - * @return mixed - */ - public function __call($method, $parameters) - { - if (isset($this->macros[$method])) { - array_unshift($parameters, $this); - - return call_user_func_array($this->macros[$method], $parameters); - } elseif (method_exists($this->model, $scope = 'scope'.ucfirst($method))) { - return $this->callScope($scope, $parameters); - } - - $result = call_user_func_array([$this->query, $method], $parameters); - - return in_array($method, $this->passthru) ? $result : $this; - } - - /** - * Force a clone of the underlying query builder when cloning. - */ - public function __clone() - { - $this->query = clone $this->query; - } -} diff --git a/src/Eloquent/Collection.php b/src/Eloquent/Collection.php deleted file mode 100644 index f303adb9..00000000 --- a/src/Eloquent/Collection.php +++ /dev/null @@ -1,255 +0,0 @@ -getKey(); - } - - return Arr::first($this->items, function ($itemKey, $model) use ($key) { - return $model->getKey() == $key; - - }, $default); - } - - /** - * Load a set of relationships onto the collection. - * - * @param mixed $relations - * - * @return $this - */ - public function load($relations) - { - if (count($this->items) > 0) { - if (is_string($relations)) { - $relations = func_get_args(); - } - - $query = $this->first()->newQuery()->with($relations); - - $this->items = $query->eagerLoadRelations($this->items); - } - - return $this; - } - - /** - * Add an item to the collection. - * - * @param mixed $item - * - * @return $this - */ - public function add($item) - { - $this->items[] = $item; - - return $this; - } - - /** - * Determine if a key exists in the collection. - * @param mixed $key - * @param mixed $operator - * @param mixed $value - * @return bool - */ - public function contains($key, $operator = null, $value = null) - { - if (func_num_args() == 1) { - if ($this->useAsCallable($key)) { - return parent::contains($key); - } - - $key = $key instanceof Model ? $key->getKey() : $key; - - return parent::contains(function ($k, $m) use ($key) { - return $m->getKey() == $key; - }); - } - - if (func_num_args() == 2) { - return parent::contains($key, $operator); - } - - - return parent::contains($key, $operator, $value); - } - - /** - * Fetch a nested element of the collection. - * - * @param string $key - * - * @return static - * - * @deprecated since version 5.1. Use pluck instead. - */ - public function fetch($key) - { - return new static(Arr::fetch($this->toArray(), $key)); - } - - /** - * Get the array of primary keys. - * - * @return array - */ - public function modelKeys() - { - return array_map(function ($m) { return $m->getKey(); }, $this->items); - } - - /** - * Merge the collection with the given items. - * - * @param \ArrayAccess|array $items - * - * @return static - */ - public function merge($items) - { - $dictionary = $this->getDictionary(); - - foreach ($items as $item) { - $dictionary[$item->getKey()] = $item; - } - - return new static(array_values($dictionary)); - } - - /** - * Diff the collection with the given items. - * - * @param \ArrayAccess|array $items - * - * @return static - */ - public function diff($items) - { - $diff = new static(); - - $dictionary = $this->getDictionary($items); - - foreach ($this->items as $item) { - if (!isset($dictionary[$item->getKey()])) { - $diff->add($item); - } - } - - return $diff; - } - - /** - * Intersect the collection with the given items. - * - * @param \ArrayAccess|array $items - * - * @return static - */ - public function intersect($items) - { - $intersect = new static(); - - $dictionary = $this->getDictionary($items); - - foreach ($this->items as $item) { - if (isset($dictionary[$item->getKey()])) { - $intersect->add($item); - } - } - - return $intersect; - } - - /** - * Return only unique items from the collection array. - * - * @param string|callable|null $key - * @param bool $strict - * - * @return static - */ - public function unique($key = null, $strict = false) - { - if (!is_null($key)) { - return parent::unique($key, $strict); - } - - return new static(array_values($this->getDictionary())); - } - - /** - * Returns only the models from the collection with the specified keys. - * - * @param mixed $keys - * - * @return static - */ - public function only($keys) - { - $dictionary = Arr::only($this->getDictionary(), $keys); - - return new static(array_values($dictionary)); - } - - /** - * Returns all models in the collection except the models with specified keys. - * - * @param mixed $keys - * - * @return static - */ - public function except($keys) - { - $dictionary = array_except($this->getDictionary(), $keys); - - return new static(array_values($dictionary)); - } - - /** - * Get a dictionary keyed by primary keys. - * - * @param \ArrayAccess|array $items - * - * @return array - */ - public function getDictionary($items = null) - { - $items = is_null($items) ? $this->items : $items; - - $dictionary = []; - - foreach ($items as $value) { - $dictionary[$value->getKey()] = $value; - } - - return $dictionary; - } - - /** - * Get a base Support collection instance from this collection. - * - * @return \Illuminate\Support\Collection - */ - public function toBase() - { - return new BaseCollection($this->items); - } -} diff --git a/src/Eloquent/Edges/Delegate.php b/src/Eloquent/Edges/Delegate.php deleted file mode 100644 index 76e747e5..00000000 --- a/src/Eloquent/Edges/Delegate.php +++ /dev/null @@ -1,291 +0,0 @@ -query = $query; - $model = $query->getModel(); - - // Setup the database connection and client. - $this->connection = $model->getConnection(); - $this->client = $this->connection->getClient(); - } - - /** - * Get a new Finder instance. - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Finder - */ - public function newFinder() - { - return new Finder($this->query); - } - - protected function getRelationshipAttributes( - $startModel, - $endModel = null, - array $properties = [], - $type = null, - $direction = null - ) { - $attributes = [ - 'label' => isset($this->type) ? $this->type : $type, - 'direction' => isset($this->direction) ? $this->direction : $direction, - 'properties' => $properties, - 'start' => [ - 'id' => [ - 'key' => $startModel->getKeyName(), - 'value' => $startModel->getKey(), - ], - 'label' => $startModel->getDefaultNodeLabel(), - 'properties' => $this->getModelProperties($startModel), - ], - ]; - - if ($endModel) { - $attributes['end'] = [ - 'id' => [ - 'key' => $endModel->getKeyName(), - 'value' => $endModel->getKey(), - ], - 'label' => $endModel->getDefaultNodeLabel(), - 'properties' => $this->getModelProperties($endModel), - ]; - } - - return $attributes; - } - - /** - * Get the model's attributes as query-able properties. - * - * @param \Vinelab\NeoEloquent\Eloquent\Model $model - * - * @return array - */ - protected function getModelProperties(Model $model) - { - $properties = $model->toArray(); - // there shouldn't be an 'id' within the attributes. - unset($properties['id']); - // node primary keys should not be passed in as properties. - unset($properties[$model->getKeyName()]); - - return $properties; - } - - /** - * Make a new Relationship instance. - * - * @param string $type - * @param \Vinelab\NeoEloquent\Eloquent\Model $startModel - * @param \Vinelab\NeoEloquent\Eloquent\Model $endModel - * @param array $properties - * - * @return Relationship - */ - protected function makeRelationship($type, $startModel, $endModel, $properties = array()) - { - $grammar = $this->query->getQuery()->getGrammar(); - $attributes = $this->getRelationshipAttributes($startModel, $endModel, $properties); - - $id = null; - if (isset($properties['id'])) { - // when there's an ID within the properties - // we will remove that so that it doesn't get - // mixed up with the properties. - $id = $properties['id']; - unset($properties['id']); - } - - return new Relationship($id, $this->asNode($startModel)->getId(), $this->asNode($endModel)->getId(), $type, new CypherMap($properties)); - } - - /** - * Get the direct relation between two models. - * - * @param \Vinelab\NeoEloquent\Eloquent\Model $parentModel - * @param \Vinelab\NeoEloquent\Eloquent\Model $relatedModel - * @param string $direction - * - * @return \Everyman\Neo4j\Relationship - */ - public function firstRelation(Model $parentModel, Model $relatedModel, $type, $direction = 'any') - { - $result = $this->firstRelationWithNodes($parentModel, $relatedModel, $type, $direction); - - if (count($result->getRecords()) > 0) { - return $result->firstRecord()->valueByIndex(0); - } - } - - /** - * @param Model $parentModel - * @param Model $relatedModel - * @param $type - * @param string $direction - * @return CypherList - */ - public function firstRelationWithNodes(Model $parentModel, Model $relatedModel, $type, $direction = 'any'): CypherList - { - $this->type = $type; - $this->start = $this->asNode($parentModel); -// $this->end = $this->asNode($relatedModel); - $this->direction = $direction; - // To get a relationship between two models we will have - // to find the Path between them so first let's transform - // them to nodes. - $grammar = $this->query->getQuery()->getGrammar(); - - // remove the ID for the related node so that we match - // the label regardless of the which node it is, matching - // any relationship of the type. - // $relatedInstance = $relatedModel->newInstance(); - - $attributes = $this->getRelationshipAttributes($parentModel, $relatedModel); - $query = $grammar->compileGetRelationship($this->query->getQuery(), $attributes); - - return $this->connection->select($query); - } - - /** - * Start a batch operation with the database. - * - * @return \Everyman\Neo4j\Batch - * - * @deprecated No Batches support in NeoClient at 1.3 release - */ - public function prepareBatch() - { - return $this->client->startBatch(); - } - - /** - * Commit the started batch operation. - * - * @return bool - * - * @throws \Vinelab\NeoEloquent\QueryException If no open batch to commit. - */ - public function commitBatch() - { - try { - return $this->client->commitBatch(); - } catch (\Exception $e) { - throw new QueryException('Error committing batch operation.', array(), $e); - } - } - - /** - * Get the direction value from the Neo4j - * client according to the direction set on - * the inheriting class,. - * - * @param string $direction - * - * @return string - * - * @deprecated 2.0 No longer using Everyman's Relationship to get the value - * of the direction constant - * - * @throws \Vinelab\NeoEloquent\Exceptions\UnknownDirectionException If the specified $direction is not one of in, out or inout - */ - public function getRealDirection($direction) - { - if (in_array($direction, ['in', 'out'])) { - $direction = strtoupper($direction); - } - - return $direction; - } - - /** - * Convert a model to a Node object. - * - * @param Model $model - * - * @return Node - */ - public function asNode(Model $model): ?Node - { - $id = $model->getKey(); - $properties = $model->toArray(); - $label = $model->getDefaultNodeLabel(); - - // The id should not be part of the properties since it is treated differently - if (isset($properties['id'])) { - unset($properties['id']); - } - - return new Node($id, new CypherList([$label]), new CypherMap($properties)); - } - - /** - * Get the NeoEloquent connection for this relation. - * - * @return \Vinelab\NeoEloquent\Connection - */ - public function getConnection() - { - return $this->connection; - } - - /** - * Set the database connection. - * - * @param \Vinelab\NeoEloquent\Connection $name - */ - public function setConnection(Connection $connection) - { - $this->connection = $connection; - } - - /** - * Get the current connection name. - * - * @return string - */ - public function getConnectionName() - { - return $this->query->getModel()->getConnectionName(); - } -} diff --git a/src/Eloquent/Edges/Edge.php b/src/Eloquent/Edges/Edge.php deleted file mode 100644 index 10bf8161..00000000 --- a/src/Eloquent/Edges/Edge.php +++ /dev/null @@ -1,765 +0,0 @@ -type = $type; - $this->parent = $parent; - $this->related = $related; - $this->unique = $unique; - $this->attributes = $attributes; - $this->finder = $this->newFinder(); - - $this->initRelation(); - } - - /** - * Initialize the relationship setting the start node, - * end node and relation type. - * - * @throws \Vinelab\NeoEloquent\Exceptions\NoEdgeDirectionException If $direction is not set on the inheriting relation. - */ - public function initRelation() - { - $this->updateTimestamps(); - - switch ($this->direction) { - case 'in': - // Make them nodes - $this->start = $this->asNode($this->related); - if ($this->parent->getKey()) { - $this->end = $this->asNode($this->parent); - } - // Setup relationship -// $this->relation = $this->makeRelationship($this->type, $this->related, $this->parent, $this->attributes); - break; - - case 'out': - // Make them nodes - $this->start = $this->asNode($this->parent); - if ($this->related->getKey()) { - $this->end = $this->asNode($this->related); - } - // Setup relationship -// $this->relation = $this->makeRelationship($this->type, $this->parent, $this->related, $this->attributes); - break; - - default: - throw new NoEdgeDirectionException(); - break; - } - } - - /** - * Get the direct relationship between - * the currently set models ($parent and $related). - * - * @return \Vinelab\NeoEloquent\Eloquent\Edge[In|Out] - */ - public function current() - { - $results = $this->finder->firstRelationWithNodes($this->parent, $this->related, $this->type, $this->direction); - - return !$results->isEmpty() ? $this->newFromRelation($results->first()) : null; - } - - /** - * Save the relationship to the database. - * - * @return bool - */ - public function save() - { - $this->updateTimestamps(); - - /* - * If this is a unique relationship we should check for an existing - * one of the same type and direction for the $parent node before saving - * and delete it, unless we are updating an existing relationship. - */ - if ($this->unique && !$this->exists()) { - $endModel = $this->related->newInstance(); - $existing = $this->firstRelationWithNodes($this->parent, $endModel, $this->type, $this->direction); - - if(!$existing->isEmpty()) { - $instance = $this->newFromRelation($existing->first()); - $instance->delete(); - } - } - - $saved = $this->saveRelationship($this->type, $this->parent, $this->related, $this->attributes); - - if ($saved) { - // Let's refresh the relation we alreay have set so that - // we make sure that it is totally in sync with the saved one. - // at this point $saved is an instance of GraphAware\Common\Result\RecordViewInterface - // that only contains the relationship as a record. - // We will pull that out of the Result instance - $this->setRelation($saved); - - return true; - } - - return false; - } - - /** - * @param string $type - * @param Model $start - * @param Model $end - * @param array $properties - */ - public function saveRelationship($type, $start, $end, $properties): CypherMap - { - $grammar = $this->query->getQuery()->getGrammar(); - $attributes = $this->getRelationshipAttributes($start, $end, $properties); - $query = $grammar->compileCreateRelationship($this->query->getQuery(), $attributes); - - return $this->connection->statement($query, [], true)->first(); - } - - /** - * Remove the relationship from the database. - * - * @return bool - */ - public function delete() - { - if ($this->relation) { - $grammar = $this->query->getQuery()->getGrammar(); - - // based on the direction, the matching between the parent model and the relation's start node - // are the inverse, same goes for the end node and the related model. - $startNode = $this->start; - $endNode = $this->end; - // this case applies only when it's an inbound relationship. - if ($this->direction === 'in') { - $startNode = $this->end; - $endNode = $this->start; - } - - $startModel = $this->query->newModelFromNode($startNode, $this->parent); - $endModel = $this->query->newModelFromNode($endNode, $this->related); - - // we need to delete any relationship b/w the start and end models - // so we only need the label out of the end model and not the ID. - $attributes = $this->getRelationshipAttributes($startModel, $endModel); - $query = $grammar->compileDeleteRelationship($this->query->getQuery(), $attributes); - - $deleted = $this->connection->affectingStatement($query, []); - } - - return (bool) (isset($deleted)) ? true : false; - } - - /** - * Create a new Relation of the current instance - * from an existing database relation. - * - * @param \GraphAware\Neo4j\Client\Formatter\Result $results - * - * @return static - */ - public function newFromRelation(CypherMap $record) - { - $instance = new static($this->query, $this->parent, $this->related, $this->type, $this->attributes, $this->unique); - - $instance->setRelation($record); - - return $instance; - } - - /** - * Get the Neo4j relationship object. - * - * @return \Everyman\Neo4j\Relationship - */ - public function getReal() - { - return $this->relation; - } - - /** - * Get the value of the relation's primary key. - * - * @return mixed - */ - public function getKey() - { - return $this->getAttribute($this->getKeyName()); - } - - /** - * Get the primary key for the model. - * - * @return string - */ - public function getKeyName() - { - return $this->primaryKey; - } - - /** - * Set a given relationship on this relation. - */ - public function setRelation(CypherMap $record) - { - $nodes = $this->getRecordNodes($record); - $relationships = $this->getRecordRelationships($record); - $relation = reset($relationships); - - // Set the relation object. - $this->relation = $relation; - - // Replace the attributes with those brought from the given relation. - $this->attributes = $relation->getProperties()->toArray(); - $this->setAttribute($this->primaryKey, $relation->getId()); - - // Set the start and end nodes. - // FIXME: See if we will need $this->start and $this->end for they've been removed. - $this->start = $this->getNodeByType($relation, $nodes, 'start'); - $this->end = $this->getNodeByType($relation, $nodes, 'end'); - - $relatedNode = ($this->isDirectionOut()) ? $this->end : $this->start; - $attributes = array_merge(['id' => $relatedNode->getId()], $relatedNode->getProperties()->toArray()); - - $this->related = $this->related->newFromBuilder($attributes); - $this->related->setConnection($this->related->getConnectionName()); - -// $this->start = $relation->getStartNode(); -// $this->end = $relation->getEndNode(); -// -// // Instantiate and fill out the related model. -// $relatedNode = ($this->isDirectionOut()) ? $this->end : $this->start; -// $attributes = array_merge(['id' => $relatedNode->getId()], $relatedNode->getProperties()); -// -// // This is an existing relationship. -// $this->related = $this->related->newFromBuilder($attributes); -// $this->related->setConnection($this->related->getConnectionName()); - } - - /** - * Fill the model with an array of attributes. - * - * @param array $attributes - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Edge[In|Out]|static - */ - public function fill(array $properties) - { - foreach ($properties as $key => $value) { - $this->setAttribute($key, $value); - } - - return $this; - } - - /** - * Set a given attribute on the relation. - * - * @param string $key - * @param mixed $value - */ - public function setAttribute($key, $value) - { - if (in_array($key, $this->getDates())) { - if ($value) { - $value = $this->fromDateTime($value); - } - } - - $this->attributes[$key] = $value; - } - - /** - * Get an attribute from the relation. - * - * @param string $key - * - * @return mixed - */ - public function getAttribute($key) - { - if (array_key_exists($key, $this->attributes)) { - $value = $this->attributes[$key]; - - if (in_array($key, $this->getDates())) { - return $this->asDateTime($value); - } - - return $value; - } - } - - /** - * Get the attributes that should be converted to dates. - * - * @return array - */ - public function getDates() - { - $defaults = array(static::CREATED_AT, static::UPDATED_AT); - - return array_merge($this->dates, $defaults); - } - - /** - * Set all the attributes of this relation. - * - * @param array $attributes - */ - public function setRawAttributes(array $attributes) - { - $this->attributes = $attributes; - } - - /** - * Get all the attributes of this relation. - * - * @return mixed - */ - public function getAttributes() - { - return $this->attributes; - } - - /** - * Get the Models of this relation. - * - * @return \Illuminate\Database\Collection - */ - public function getModels() - { - return new Collection(array($this->parent, $this->related)); - } - - /** - * Just a convenient method to get - * the parent model of this relation. - * - * @return \Vinelab\NeoEloquent\Eloquent\Model - */ - public function parent() - { - return $this->getParent(); - } - - /** - * Get the parent model of this relation. - * - * @return \Vinelab\NeoEloquent\Eloquent\Model - */ - public function getParent() - { - return $this->parent; - } - - /** - * Just a convenient function to get - * the related Model of this relation. - * - * @return \Vinelab\NeoEloquent\Eloquent\Model - */ - public function related() - { - return $this->getRelated(); - } - - /** - * Get the parent model of this relation. - * - * @return \Vinelab\NeoEloquent\Eloquent\Model - */ - public function getRelated() - { - return $this->related; - } - - /** - * Get the Nodes of this relation. - * - * @return \Illuminate\Database\Collection - */ - public function getNodes() - { - return new Collection(array($this->start, $this->end)); - } - - /** - * Determine whether this relationship is unique. - * - * @return bool - */ - public function isUnique() - { - return $this->unique; - } - - /** - * Determine whether this relation exists. - * - * @return bool - */ - public function exists() - { - $exists = false; - - if ($this->relation) { - $exists = true; - } - - return $exists; - } - - /** - * Get the format for database stored dates. - * - * @return string - */ - protected function getDateFormat() - { - return $this->getConnection()->getQueryGrammar()->getDateFormat(); - } - - /** - * Convert a DateTime to a storable string. - * - * @param \DateTime|int $value - * - * @return string - */ - public function fromDateTime($value) - { - $format = $this->getDateFormat(); - - // If the value is already a DateTime instance, we will just skip the rest of - // these checks since they will be a waste of time, and hinder performance - // when checking the field. We will just return the DateTime right away. - if ($value instanceof DateTime) { - // - } - - // If the value is totally numeric, we will assume it is a UNIX timestamp and - // format the date as such. Once we have the date in DateTime form we will - // format it according to the proper format for the database connection. - elseif (is_numeric($value)) { - $value = Carbon::createFromTimestamp($value); - } - - // If the value is in simple year, month, day format, we will format it using - // that setup. This is for simple "date" fields which do not have hours on - // the field. This conveniently picks up those dates and format correct. - elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value)) { - $value = Carbon::createFromFormat('Y-m-d', $value)->startOfDay(); - } - - // If this value is some other type of string, we'll create the DateTime with - // the format used by the database connection. Once we get the instance we - // can return back the finally formatted DateTime instances to the devs. - elseif (!$value instanceof DateTime) { - $value = Carbon::createFromFormat($format, $value); - } - - return $value->format($format); - } - - /** - * Return a timestamp as DateTime object. - * - * @param mixed $value - * - * @return \Carbon\Carbon - */ - protected function asDateTime($value) - { - // If this value is an integer, we will assume it is a UNIX timestamp's value - // and format a Carbon object from this timestamp. This allows flexibility - // when defining your date fields as they might be UNIX timestamps here. - if (is_numeric($value)) { - return Carbon::createFromTimestamp($value); - } - - // If the value is in simply year, month, day format, we will instantiate the - // Carbon instances from that format. Again, this provides for simple date - // fields on the database, while still supporting Carbonized conversion. - elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value)) { - return Carbon::createFromFormat('Y-m-d', $value)->startOfDay(); - } - - // Finally, we will just assume this date is in the format used by default on - // the database connection and use that format to create the Carbon object - // that is returned back out to the developers after we convert it here. - elseif (!$value instanceof DateTime) { - $format = $this->getDateFormat(); - - return Carbon::createFromFormat($format, $value); - } - - return Carbon::instance($value); - } - - /** - * Convert the model instance to an array. - * - * @return array - */ - public function toArray() - { - return (array) $this->attributes; - } - - /** - * Get the left node of the relationship. - * - * @return \Everyman\Neo4j\Node - */ - public function getStartNode() - { - return $this->start; - } - - /** - * Get the end Node of the relationship. - * - * @return \Everyman\Neo4j\Node - */ - public function getEndNode() - { - return $this->end; - } - - /** - * Update the creation and update timestamps. - */ - protected function updateTimestamps() - { - if ($this->parent->timestamps) { - $time = $this->freshTimestamp(); - - $this->setUpdatedAt($time); - - if (!$this->exists()) { - $this->setCreatedAt($time); - } - } - } - - /** - * Set the value of the "created at" attribute. - * - * @param mixed $value - */ - public function setCreatedAt($value) - { - $this->{static::CREATED_AT} = $value; - } - - /** - * Set the value of the "updated at" attribute. - * - * @param mixed $value - */ - public function setUpdatedAt($value) - { - $this->{static::UPDATED_AT} = $value; - } - - /** - * Get a fresh timestamp for the model. - * - * @return \Carbon\Carbon - */ - public function freshTimestamp() - { - return new Carbon(); - } - - /** - * Determine whether the direction of the relationship is 'out'. - * - * @return bool - */ - public function isDirectionOut() - { - return $this->direction == 'out'; - } - - /** - * Determine whether the direction of the relationship is 'in'. - * - * @return bool [description] - */ - public function isDirectionIn() - { - return $this->direction == 'in'; - } - - /** - * Determine whether the direction of the relationship is 'any'. - * - * @return bool - */ - public function isDirectionAny() - { - return $this->direction == 'any'; - } - - /** - * Dynamically set attributes on the relation. - * - * @param string $key - * @param mixed $value - */ - public function __set($key, $value) - { - $this->setAttribute($key, $value); - } - - /** - * Dynamically retrieve attributes on the relation. - * - * @param string $key - * - * @return mixed - */ - public function __get($key) - { - return $this->getAttribute($key); - } -} diff --git a/src/Eloquent/Edges/EdgeIn.php b/src/Eloquent/Edges/EdgeIn.php deleted file mode 100644 index 7917350c..00000000 --- a/src/Eloquent/Edges/EdgeIn.php +++ /dev/null @@ -1,8 +0,0 @@ -firstRelationWithNodes($parentModel, $relatedModel, $type, $direction); - - // Let's stop here if there is no relationship between them. - if ($results->isEmpty()) { - return null; - } - - $record = $results->first(); - - // Now we can return the determined edge out of the relation and direction. - return $this->edgeFromRelationWithDirection($record, $parentModel, $relatedModel, $direction); - } - - /** - * Get the edges between two models. - * - * @param \Vinelab\NeoEloquent\Eloquent\Model $parent - * @param \Vinelab\NeoEloquent\Eloquent\Model $related - * @param string|array $type - * - * @return \Illuminate\Database\Eloquent\Collection - */ - public function get(Model $parent, Model $related, $type, $direction) - { - // Get the relationships for the parent node of the given type. - $records = $this->firstRelationWithNodes($parent, $related, $type, $direction); - - $edges = []; - // Collect the edges out of the found relationships. - foreach ($records as $record) { - // Now that we have the direction and the relationship all we need to do is generate the edge - // and add it to our collection of edges. - $edges[] = $this->edgeFromRelationWithDirection($record, $parent, $related, $direction); - } - - return new Collection($edges); - } - - /** - * Delete the current relation in the query. - * - * @return bool - */ - public function delete($shouldKeepEndNode) - { - $builder = $this->query->getQuery(); - $grammar = $builder->getGrammar(); - - $cypher = $grammar->compileDelete($builder, true, $shouldKeepEndNode); - - $result = $this->connection->delete($cypher, $builder->getBindings()); - - if ($result instanceof GraphawareResult) { - $result = true; - } - - return $result; - } - - /** - * Get the first HyperEdge between three models. - * - * @param \Vinelab\NeoEloquent\Eloquent\Model $parent - * @param \Vinelab\NeoEloquent\Eloquent\Model $related - * @param \Vinelab\NeoEloquent\Eloquent\Model $morph - * @param string $type - * @param string $morphType - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge - */ - public function hyperFirst($parent, $related, $morph, $type, $morphType) - { - $left = $this->first($parent, $related, $type, 'out'); - $right = $this->first($related, $morph, $morphType, 'out'); - - $edge = new HyperEdge($this->query, $parent, $type, $related, $morphType, $morph); - if ($left) { - $edge->setLeft($left); - } - if ($right) { - $edge->setRight($right); - } - - return $edge; - } - - /** - * Get the direction of a relationship out of a Relation instance. - * - * @param \GraphAware\Neo4j\Client\Formatter\Result $results - * @param \Vinelab\NeoEloquent\Eloquent\Model $parent - * @param \Vinelab\NeoEloquent\Eloquent\Model $related - * - * @return string Either 'in' or 'out' - */ - public function directionFromRelation(Result $results, Model $parent, Model $related) - { - // We will match the ids of the parent model and the start node of the relationship - // and if they match we know that the direction is outgoing, incoming otherwise. - $nodes = $this->getNodeRecords($results); - $relations = $this->getRelationshipRecords($results); - $relation = reset($relations); - - $startNode = $this->getNodeByType($relation, $nodes); - - // We will start by considering the relationship direction to be 'incoming' until - // we match and find otherwise. - $direction = 'in'; - - $id = ($parent->getKeyName() === 'id') ? $id = $relation->startNodeIdentity() : $startNode->value($parent->getKeyName()); - - if ($id === $parent->getKey()) { - $direction = 'out'; - } - - return $direction; - } - - /** - * Get the Edge instance out of a Relationship based on a direction. - * - * @param CypherMap $record - * @param Model $parent - * @param Model $related - * @param string $direction can be 'in' or 'out' - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Edge[In|Out] - */ - public function edgeFromRelationWithDirection(CypherMap $record, Model $parent, Model $related, $direction) - { - $relationships = $this->getRecordRelationships($record); - /** @var Relationship $relation */ - $relation = reset($relationships); - - if ($relation) { - // Based on the direction we are now able to construct the edge class name and call for - // an instance of it then pass it the actual relationship that was previously found. - $class = $this->getEdgeClass($direction); - /** @var Edge $edge */ - $edge = new $class($this->query, $parent, $related, $relation->getType()); - $edge->setRelation($record); - - return $edge; - } - } - - public function getModelRelationsForType(Model $startModel, Model $endModel, $type = null, $direction = null) - { - // Determine the direction, the real one! - $direction = $this->getRealDirection($direction); - - $grammar = $this->query->getQuery()->getGrammar(); - - $query = $grammar->compileGetRelationship( - $this->query->getQuery(), - $this->getRelationshipAttributes($startModel, $endModel, [], $type, $direction) - ); - - $result = $this->connection->statement($query, [], true); - - return $this->getRelationshipRecords($result); - } - - /** - * Get the edge class name for a direction. - * - * @param string $direction - * - * @return string - */ - public function getEdgeClass($direction) - { - return __NAMESPACE__.'\Edge'.ucfirst(mb_strtolower($direction)); - } -} diff --git a/src/Eloquent/Edges/HyperEdge.php b/src/Eloquent/Edges/HyperEdge.php deleted file mode 100644 index fcd045d9..00000000 --- a/src/Eloquent/Edges/HyperEdge.php +++ /dev/null @@ -1,182 +0,0 @@ -morph = $morph; - $this->morphType = $morphType; - - // This is not a unique relationship since it involves multiple models. - $unique = false; - - parent::__construct($query, $parent, $related, $type, $attributes, $unique); - } - - /** - * Initialize the relationship by setting up nodes and edges,. - * - * - * @throws \Vinelab\NeoEloquent\NoEdgeDirectionException If $direction is not set on the inheriting relation. - */ - public function initRelation() - { - // Turn models into nodes - $this->start = $this->asNode($this->parent); - $this->hyper = $this->asNode($this->related); - $this->end = $this->asNode($this->morph); - - // Not a unique relationship since it involves multiple models. - $unique = false; - - // Setup left and right edges - $this->left = new EdgeOut($this->query, $this->parent, $this->related, $this->type, $this->attributes, $unique); - $this->right = new EdgeOut($this->query, $this->related, $this->morph, $this->morphType, $this->attributes, $unique); - // Set the morph type to the relationship so that we know who we're talking to. - $this->right->morph_type = get_class($this->morph); - } - - /** - * Get the left side Edge of this relationship. - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut - */ - public function left() - { - return $this->left; - } - - /** - * Set the left side Edge of this relation. - * - * @param \Vinelab\NeoEloquent\Eloquent\Edges\Edge $left - */ - public function setLeft($left) - { - $this->left = $left; - } - - /** - * Get the right side Edge of this relationship. - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut - */ - public function right() - { - return $this->right; - } - - /** - * Set the right side Edge of this relationship. - * - * @param \Vinelab\NeoEloquent\Eloquent\Edges\Edge $right - */ - public function setRight($right) - { - $this->right = $right; - } - - /** - * Get the hyper model of the relationship. - * - * @return \Vinelab\NeoEloquent\Eloquent\Model - */ - public function hyper() - { - return $this->getRelated(); - } - - /** - * Save the relationship to the database. - * - * @return bool - */ - public function save() - { - $savedLeft = $this->left->save(); - $savedRight = $this->right->save(); - - return $savedLeft && $savedRight; - } - - /** - * Remove the relationship from the database. - * - * @return bool - */ - public function delete() - { - if ($this->exists()) { - $deletedLeft = $this->left->delete(); - $deletedRight = $this->right->delete(); - - return $deletedLeft && $deletedRight; - } - - return false; - } - - /** - * Determine whether this relation exists. - * - * @return bool - */ - public function exists() - { - return $this->left->exists() && $this->right->exists(); - } -} diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php deleted file mode 100644 index c1ba99e7..00000000 --- a/src/Eloquent/Model.php +++ /dev/null @@ -1,3746 +0,0 @@ -bootIfNotBooted(); - - $this->syncOriginal(); - - $this->fill($attributes); - } - - /** - * Check if the model needs to be booted and if so, do it. - */ - protected function bootIfNotBooted() - { - $class = get_class($this); - - if (!isset(static::$booted[$class])) { - static::$booted[$class] = true; - - $this->fireModelEvent('booting', false); - - static::boot(); - - $this->fireModelEvent('booted', false); - } - } - - /** - * The "booting" method of the model. - */ - protected static function boot() - { - static::bootTraits(); - } - - /** - * Boot all of the bootable traits on the model. - */ - protected static function bootTraits() - { - foreach (class_uses_recursive(get_called_class()) as $trait) { - if (method_exists(get_called_class(), $method = 'boot'.class_basename($trait))) { - forward_static_call([get_called_class(), $method]); - } - } - } - - /** - * Clear the list of booted models so they will be re-booted. - */ - public static function clearBootedModels() - { - static::$booted = []; - } - - /** - * Register a new global scope on the model. - * - * @param \Vinelab\NeoEloquent\Eloquent\ScopeInterface $scope - */ - public static function addGlobalScope(ScopeInterface $scope) - { - static::$globalScopes[get_called_class()][get_class($scope)] = $scope; - } - - /** - * Determine if a model has a global scope. - * - * @param \Illuminate\Database\Eloquent\ScopeInterface $scope - * - * @return bool - */ - public static function hasGlobalScope($scope) - { - return !is_null(static::getGlobalScope($scope)); - } - - /** - * Get a global scope registered with the model. - * - * @param \Illuminate\Database\Eloquent\ScopeInterface $scope - * - * @return \Illuminate\Database\Eloquent\ScopeInterface|null - */ - public static function getGlobalScope($scope) - { - return Arr::first(static::$globalScopes[get_called_class()], function ($key, $value) use ($scope) { - return $scope instanceof $value; - }); - } - - /** - * Get the global scopes for this class instance. - * - * @return \Illuminate\Database\Eloquent\ScopeInterface[] - */ - public function getGlobalScopes() - { - return Arr::get(static::$globalScopes, get_class($this), []); - } - - /** - * Register an observer with the Model. - * - * @param object|string $class - * @param int $priority - */ - public static function observe($class, $priority = 0) - { - $instance = new static(); - - $className = is_string($class) ? $class : get_class($class); - - // When registering a model observer, we will spin through the possible events - // and determine if this observer has that method. If it does, we will hook - // it into the model's event system, making it convenient to watch these. - foreach ($instance->getObservableEvents() as $event) { - if (method_exists($class, $event)) { - static::registerModelEvent($event, $className.'@'.$event, $priority); - } - } - } - - /** - * Fill the model with an array of attributes. - * - * @param array $attributes - * - * @return $this - * - * @throws \Illuminate\Database\Eloquent\MassAssignmentException - */ - public function fill(array $attributes) - { - $totallyGuarded = $this->totallyGuarded(); - - foreach ($this->fillableFromArray($attributes) as $key => $value) { - - // The developers may choose to place some attributes in the "fillable" - // array, which means only those attributes may be set through mass - // assignment to the model, and all others will just be ignored. - if ($this->isFillable($key)) { - $this->setAttribute($key, $value); - } elseif ($totallyGuarded) { - throw new MassAssignmentException($key); - } - } - - return $this; - } - - /** - * Fill the model with an array of attributes. Force mass assignment. - * - * @param array $attributes - * - * @return $this - */ - public function forceFill(array $attributes) - { - // Since some versions of PHP have a bug that prevents it from properly - // binding the late static context in a closure, we will first store - // the model in a variable, which we will then use in the closure. - $model = $this; - - return static::unguarded(function () use ($model, $attributes) { - return $model->fill($attributes); - }); - } - - /** - * Get the fillable attributes of a given array. - * - * @param array $attributes - * - * @return array - */ - protected function fillableFromArray(array $attributes) - { - if (count($this->fillable) > 0 && !static::$unguarded) { - return array_intersect_key($attributes, array_flip($this->fillable)); - } - - return $attributes; - } - - /** - * Create a new instance of the given model. - * - * @param array $attributes - * @param bool $exists - * - * @return static - */ - public function newInstance($attributes = [], $exists = false) - { - // This method just provides a convenient way for us to generate fresh model - // instances of this current model. It is particularly useful during the - // hydration of new objects via the Eloquent query builder instances. - $model = new static((array) $attributes); - - $model->exists = $exists; - - return $model; - } - - /** - * Create a new model instance that is existing. - * - * @param array $attributes - * @param string|null $connection - * - * @return static - */ - public function newFromBuilder($attributes = [], $connection = null) - { - $model = $this->newInstance([], true); - - $model->setRawAttributes((array) $attributes, true); - - $model->setConnection($connection ?: $this->connection); - - return $model; - } - - /** - * Create a collection of models from plain arrays. - * - * @param array $items - * @param string|null $connection - * - * @return \Illuminate\Database\Eloquent\Collection - */ - public static function hydrate(array $items, $connection = null) - { - $instance = (new static())->setConnection($connection); - - $items = array_map(function ($item) use ($instance) { - return $instance->newFromBuilder($item); - }, $items); - - return $instance->newCollection($items); - } - - /** - * Create a collection of models from a raw query. - * - * @param string $query - * @param array $bindings - * @param string|null $connection - * - * @return \Illuminate\Database\Eloquent\Collection - */ - public static function hydrateRaw($query, $bindings = [], $connection = null) - { - $instance = (new static())->setConnection($connection); - - $items = $instance->getConnection()->select($query, $bindings); - - return static::hydrate($items, $connection); - } - - /** - * Save a new model and return the instance. - * - * @param array $attributes - * - * @return static - */ - public static function create(array $attributes = []) - { - $model = new static($attributes); - - $model->save(); - - return $model; - } - - /** - * Save a new model and return the instance. Allow mass-assignment. - * - * @param array $attributes - * - * @return static - */ - public static function forceCreate(array $attributes) - { - // Since some versions of PHP have a bug that prevents it from properly - // binding the late static context in a closure, we will first store - // the model in a variable, which we will then use in the closure. - $model = new static(); - - return static::unguarded(function () use ($model, $attributes) { - return $model->create($attributes); - }); - } - - /** - * Get the first record matching the attributes or create it. - * - * @param array $attributes - * - * @return static - */ - public static function firstOrCreate(array $attributes) - { - if (!is_null($instance = static::where($attributes)->first())) { - return $instance; - } - - return static::create($attributes); - } - - /** - * Get the first record matching the attributes or instantiate it. - * - * @param array $attributes - * - * @return static - */ - public static function firstOrNew(array $attributes) - { - if (!is_null($instance = static::where($attributes)->first())) { - return $instance; - } - - return new static($attributes); - } - - /** - * Create or update a record matching the attributes, and fill it with values. - * - * @param array $attributes - * @param array $values - * - * @return static - */ - public static function updateOrCreate(array $attributes, array $values = []) - { - $instance = static::firstOrNew($attributes); - - $instance->fill($values)->save(); - - return $instance; - } - - /** - * Begin querying the model. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public static function query() - { - return (new static())->newQuery(); - } - - /** - * Begin querying the model on a given connection. - * - * @param string|null $connection - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public static function on($connection = null) - { - // First we will just create a fresh instance of this model, and then we can - // set the connection on the model so that it is be used for the queries - // we execute, as well as being set on each relationship we retrieve. - $instance = new static(); - - $instance->setConnection($connection); - - return $instance->newQuery(); - } - - /** - * Get all of the models from the database. - * - * @param array $columns - * - * @return \Illuminate\Database\Eloquent\Collection|static[] - */ - public static function all($columns = ['*']) - { - $columns = is_array($columns) ? $columns : func_get_args(); - - $instance = new static(); - - return $instance->newQuery()->get($columns); - } - - /** - * Find a model by its primary key or return new static. - * - * @param mixed $id - * @param array $columns - * - * @return \Illuminate\Support\Collection|static - */ - public static function findOrNew($id, $columns = ['*']) - { - if (!is_null($model = static::find($id, $columns))) { - return $model; - } - - return new static(); - } - - /** - * Reload a fresh model instance from the database. - * - * @param array $with - * - * @return $this - */ - public function fresh(array $with = []) - { - if (!$this->exists) { - return; - } - - $key = $this->getKeyName(); - - return static::with($with)->where($key, $this->getKey())->first(); - } - - /** - * Eager load relations on the model. - * - * @param array|string $relations - * - * @return $this - */ - public function load($relations) - { - if (is_string($relations)) { - $relations = func_get_args(); - } - - $query = $this->newQuery()->with($relations); - - $query->eagerLoadRelations([$this]); - - return $this; - } - - /** - * Begin querying a model with eager loading. - * - * @param array|string $relations - * - * @return \Illuminate\Database\Eloquent\Builder|static - */ - public static function with($relations) - { - if (is_string($relations)) { - $relations = func_get_args(); - } - - $instance = new static(); - - return $instance->newQuery()->with($relations); - } - - /** - * Append attributes to query when building a query. - * - * @param array|string $attributes - * - * @return $this - */ - public function append($attributes) - { - if (is_string($attributes)) { - $attributes = func_get_args(); - } - - $this->appends = array_unique( - array_merge($this->appends, $attributes) - ); - - return $this; - } - - /** - * Set the node label for this model. - * - * @param string|array $labels - */ - public function setLabel($label) - { - return $this->label = $label; - } - - /** - * @override - * Get the node label for this model. - * - * @return string|array - */ - public function getLabel() - { - return $this->label; - } - - /** - * @override - * Create a new Eloquent query builder for the model. - * - * @param Vinelab\NeoEloquent\Query\Builder $query - * - * @return Vinelab\NeoEloquent\Eloquent\Builder|static - */ - public function newEloquentBuilder($query) - { - return new EloquentBuilder($query); - } - - /** - * @override - * Get a new query builder instance for the connection. - * - * @return Vinelab\NeoEloquent\Query\Builder - */ - protected function newBaseQueryBuilder() - { - $conn = $this->getConnection(); - - $grammar = $conn->getQueryGrammar(); - - return new QueryBuilder($conn, $grammar); - } - - /** - * Get the node labels. - * - * @return array - */ - public function getDefaultNodeLabel() - { - // by default we take the $label, otherwise we consider $table - // for Eloquent's backward compatibility - $label = (empty($this->label)) ? $this->table : $this->label; - - // The label is accepted as an array for a convenience so we need to - // convert it to a string separated by ':' following Neo4j's labels - if (is_array($label) && !empty($label)) { - return $label; - } - - // since this is not an array, it is assumed to be a string - // we check to see if it follows neo4j's labels naming (User:Fan) - // and return an array exploded from the ':' - if (!empty($label)) { - $label = array_filter(explode(':', $label)); - - // This trick re-indexes the array - array_splice($label, 0, 0); - - return $label; - } - - // Since there was no label for this model - // we take the fully qualified (namespaced) class name and - // pluck out backslashes to get a clean 'WordsUp' class name and use it as default - return array(str_replace('\\', '', get_class($this))); - } - - /** - * @override - * Get the table associated with the model. - * - * @return string - */ - public function nodeLabel() - { - return $this->getDefaultNodeLabel(); - } - - /** - * @override - * Define an inverse one-to-one or many relationship. - * - * @param string $related - * @param string $foreignKey - * @param string $otherKey - * @param string $relation - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function belongsTo($related, $foreignKey = null, $otherKey = null, $relation = null) - { - // If no relation name was given, we will use this debug backtrace to extract - // the calling method's name and use that as the relationship name as most - // of the time this will be what we desire to use for the relationships. - if (is_null($relation)) { - list(, $caller) = debug_backtrace(false); - - $relation = $caller['function']; - } - - // If no foreign key was supplied, we can use a backtrace to guess the proper - // foreign key name by using the name of the calling class, which - // will be uppercased and used as a relationship label - if (is_null($foreignKey)) { - $foreignKey = strtoupper($caller['class']); - } - - $instance = new $related(); - - // Once we have the foreign key names, we'll just create a new Eloquent query - // for the related models and returns the relationship instance which will - // actually be responsible for retrieving and hydrating every relations. - $query = $instance->newQuery(); - - $otherKey = $otherKey ?: $instance->getKeyName(); - - return new BelongsTo($query, $this, $foreignKey, $otherKey, $relation); - } - - /** - * @override - * Define a one-to-one relationship. - * - * @param string $related - * @param string $foreignKey - * @param string $localKey - * - * @return \Illuminate\Database\Eloquent\Relations\HasOne - */ - public function hasOne($related, $foreignKey = null, $otherKey = null, $relation = null) - { - // If no relation name was given, we will use this debug backtrace to extract - // the calling method's name and use that as the relationship name as most - // of the time this will be what we desire to use for the relationships. - if (is_null($relation)) { - list(, $caller) = debug_backtrace(false); - - $relation = $caller['function']; - } - - // If no foreign key was supplied, we can use a backtrace to guess the proper - // foreign key name by using the name of the calling class, which - // will be uppercased and used as a relationship label - if (is_null($foreignKey)) { - $foreignKey = strtoupper($caller['class']); - } - - $instance = new $related(); - - // Once we have the foreign key names, we'll just create a new Eloquent query - // for the related models and returns the relationship instance which will - // actually be responsible for retrieving and hydrating every relations. - $query = $instance->newQuery(); - - $otherKey = $otherKey ?: $instance->getKeyName(); - - return new HasOne($query, $this, $foreignKey, $otherKey, $relation); - } - - /** - * @override - * Define a one-to-many relationship. - * - * @param string $related - * @param string $type - * @param string $key - * - * @return \Vinelab\NeoEloquent\Eloquent\Relations\HasMany - */ - public function hasMany($related, $type = null, $key = null, $relation = null) - { - // If no relation name was given, we will use this debug backtrace to extract - // the calling method's name and use that as the relationship name as most - // of the time this will be what we desire to use for the relationships. - if (is_null($relation)) { - list(, $caller) = debug_backtrace(false); - - $relation = $caller['function']; - } - - // the $type should be the UPPERCASE of the relation not the foreign key. - $type = $type ?: mb_strtoupper($relation); - - $instance = new $related(); - - $key = $key ?: $this->getKeyName(); - - return new HasMany($instance->newQuery(), $this, $type, $key, $relation); - } - - /** - * @override - * Define a many-to-many relationship. - * - * @param string $related - * @param string $type - * @param string $key - * @param string $relation - * - * @return \Vinelab\NeoEloquent\Eloquent\Relations\BelongsToMany - */ - public function belongsToMany( - $related, - $table = null, - $foreignPivotKey = null, - $relatedPivotKey = null, - $parentKey = null, - $relatedKey = null, - $relation = null - ) { - // To escape the error: - // PHP Strict standards: Declaration of Vinelab\NeoEloquent\Eloquent\Model::belongsToMany() should be - // compatible with Illuminate\Database\Eloquent\Model::belongsToMany() - // We'll just map them in with the variables we want. - $type = $table; - $key = $foreignPivotKey; - $relation = $relatedPivotKey; - // If no relation name was given, we will use this debug backtrace to extract - // the calling method's name and use that as the relationship name as most - // of the time this will be what we desire to use for the relationships. - if (is_null($relation)) { - list(, $caller) = debug_backtrace(false); - - $relation = $caller['function']; - } - - // If no $key was provided we will consider it the key name of this model. - $key = $key ?: $this->getKeyName(); - - // If no relationship type was provided, we can use the previously traced back - // $relation being the function name that called this method and using it in its - // all uppercase form. - if (is_null($type)) { - $type = mb_strtoupper($relation); - } - - $instance = new $related(); - - // Now we're ready to create a new query builder for the related model and - // the relationship instances for the relation. The relations will set - // appropriate query constraint and entirely manages the hydrations. - $query = $instance->newQuery(); - - return new BelongsToMany($query, $this, $type, $key, $relation); - } - - /** - * @override - * Create a new HyperMorph relationship. - * - * @param \Vinelab\NeoEloquent\Eloquent\Model $model - * @param string $related - * @param string $type - * @param string $morphType - * @param string $relation - * @param string $key - * - * @return \Vinelab\NeoEloquent\Eloquent\Relations\HyperMorph - */ - public function hyperMorph($model, $related, $type = null, $morphType = null, $relation = null, $key = null) - { - // If no relation name was given, we will use this debug backtrace to extract - // the calling method's name and use that as the relationship name as most - // of the time this will be what we desire to use for the relationships. - if (is_null($relation)) { - list(, $caller) = debug_backtrace(false); - - $relation = $caller['function']; - } - - // If no $key was provided we will consider it the key name of this model. - $key = $key ?: $this->getKeyName(); - - // If no relationship type was provided, we can use the previously traced back - // $relation being the function name that called this method and using it in its - // all uppercase form. - if (is_null($type)) { - $type = mb_strtoupper($relation); - } - - $instance = new $related(); - - // Now we're ready to create a new query builder for the related model and - // the relationship instances for the relation. The relations will set - // appropriate query constraint and entirely manages the hydrations. - $query = $instance->newQuery(); - - return new HyperMorph($query, $this, $model, $type, $morphType, $key, $relation); - } - - /** - * @override - * Define a many-to-many relationship. - * - * @param string $related - * @param string $type - * @param string $key - * @param string $relation - * - * @return \Vinelab\NeoEloquent\Eloquent\Relations\MorphMany - */ - public function morphMany($related, $name, $type = null, $id = null, $localKey = null) - { - // To escape the error: - // Strict standards: Declaration of Vinelab\NeoEloquent\Eloquent\Model::morphMany() should be - // compatible with Illuminate\Database\Eloquent\Model::morphMany() - // We'll just map them in with the variables we want. - $relationType = $name; - $key = $type; - $relation = $id; - - // If no relation name was given, we will use this debug backtrace to extract - // the calling method's name and use that as the relationship name as most - // of the time this will be what we desire to use for the relationships. - if (is_null($relation)) { - list(, $caller) = debug_backtrace(false); - - $relation = $caller['function']; - } - - // If no $key was provided we will consider it the key name of this model. - $key = $key ?: $this->getKeyName(); - - // If no relationship type was provided, we can use the previously traced back - // $relation being the function name that called this method and using it in its - // all uppercase form. - if (is_null($relationType)) { - $relationType = mb_strtoupper($relation); - } - - $instance = new $related(); - - // Now we're ready to create a new query builder for the related model and - // the relationship instances for the relation. The relations will set - // appropriate query constraint and entirely manages the hydrations. - $query = $instance->newQuery(); - - return new MorphMany($query, $this, $relationType, $key, $relation); - } - - /** - * Define a polymorphic one-to-one relationship. - * - * @param string $related - * @param string $name - * @param string $type - * @param string $id - * @param string $localKey - * - * @return \Illuminate\Database\Eloquent\Relations\MorphOne - */ - public function morphOne($related, $name, $type = null, $id = null, $localKey = null) - { - $instance = new $related(); - - list($type, $id) = $this->getMorphs($name, $type, $id); - - $table = $instance->nodeLabel(); - - $localKey = $localKey ?: $this->getKeyName(); - - return new MorphOne($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey); - } - - /** - * @override - * Create an inverse one-to-one polymorphic relationship with specified model and relation. - * - * @param \Vinelab\NeoEloquent\Eloquent\Model $related - * @param string $type - * @param string $key - * @param string $relation - * - * @return \Vinelab\NeoEloquent\Eloquent\Relations\MorphedByOne - */ - public function morphedByOne($related, $type, $key = null, $relation = null) - { - // If no relation name was given, we will use this debug backtrace to extract - // the calling method's name and use that as the relationship name as most - // of the time this will be what we desire to use for the relationships. - if (is_null($relation)) { - list(, $caller) = debug_backtrace(false); - - $relation = $caller['function']; - } - - // If no $key was provided we will consider it the key name of this model. - $key = $key ?: $this->getKeyName(); - - // If no relationship type was provided, we can use the previously traced back - // $relation being the function name that called this method and using it in its - // all uppercase form. - if (is_null($type)) { - $type = mb_strtoupper($relation); - } - - $instance = new $related(); - - // Now we're ready to create a new query builder for the related model and - // the relationship instances for the relation. The relations will set - // appropriate query constraint and entirely manages the hydrations. - $query = $instance->newQuery(); - - return new MorphedByOne($query, $this, $type, $key, $relation); - } - - /** - * @override - * Define a polymorphic, inverse one-to-one or many relationship. - * - * @param string $name - * @param string $type - * @param string $id - * - * @return \Illuminate\Database\Eloquent\Relations\MorphTo - */ - public function morphTo($name = null, $type = null, $id = null) - { - - // When the name and the type are specified we'll return a MorphedByOne - // relationship with the given arguments since we know the kind of Model - // and relationship type we're looking for. - if ($name && $type) { - // Determine the relation function name out of the back trace - list(, $caller) = debug_backtrace(false); - $relation = $caller['function']; - - return $this->morphedByOne($name, $type, $id, $relation); - } - - // If no name is provided, we will use the backtrace to get the function name - // since that is most likely the name of the polymorphic interface. We can - // use that to get both the class and foreign key that will be utilized. - if (is_null($name)) { - list(, $caller) = debug_backtrace(false); - - $name = Str::snake($caller['function']); - } - - list($type, $id) = $this->getMorphs($name, $type, $id); - - // If the type value is null it is probably safe to assume we're eager loading - // the relationship. When that is the case we will pass in a dummy query as - // there are multiple types in the morph and we can't use single queries. - if (is_null($class = $this->$type)) { - return new MorphTo( - $this->newQuery(), $this, $id, null, $type, $name - ); - } - - // If we are not eager loading the relationship we will essentially treat this - // as a belongs-to style relationship since morph-to extends that class and - // we will pass in the appropriate values so that it behaves as expected. - else { - $instance = new $class(); - - return new MorphTo( - with($instance)->newQuery(), $this, $id, $instance->getKeyName(), $type, $name - ); - } - } - - /** - * Retrieve the fully qualified class name from a slug. - * - * @param string $class - * - * @return string - */ - public function getActualClassNameForMorph($class) - { - return array_get(Relation::morphMap(), $class, $class); - } - - /** - * Destroy the models for the given IDs. - * - * @param array|int $ids - * - * @return int - */ - public static function destroy($ids) - { - // We'll initialize a count here so we will return the total number of deletes - // for the operation. The developers can then check this number as a boolean - // type value or get this total count of records deleted for logging, etc. - $count = 0; - - $ids = is_array($ids) ? $ids : func_get_args(); - - $instance = new static(); - - // We will actually pull the models from the database table and call delete on - // each of them individually so that their events get fired properly with a - // correct set of attributes in case the developers wants to check these. - $key = $instance->getKeyName(); - - foreach ($instance->whereIn($key, $ids)->get() as $model) { - if ($model->delete()) { - ++$count; - } - } - - return $count; - } - - /** - * Delete the model from the database. - * - * @return bool|null - * - * @throws \Exception - */ - public function delete() - { - if (is_null($this->getKeyName())) { - throw new Exception('No primary key defined on model.'); - } - - if ($this->exists) { - if ($this->fireModelEvent('deleting') === false) { - return false; - } - - // Here, we'll touch the owning models, verifying these timestamps get updated - // for the models. This will allow any caching to get broken on the parents - // by the timestamp. Then we will go ahead and delete the model instance. - $this->touchOwners(); - - $this->performDeleteOnModel(); - - $this->exists = false; - - // Once the model has been deleted, we will fire off the deleted event so that - // the developers may hook into post-delete operations. We will then return - // a boolean true as the delete is presumably successful on the database. - $this->fireModelEvent('deleted', false); - - return true; - } - } - - /** - * Force a hard delete on a soft deleted model. - * - * This method protects developers from running forceDelete when trait is missing. - */ - public function forceDelete() - { - return $this->delete(); - } - - /** - * Perform the actual delete query on this model instance. - */ - protected function performDeleteOnModel() - { - $this->setKeysForSaveQuery($this->newQuery())->delete(); - } - - /** - * Register a saving model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - */ - public static function saving($callback, $priority = 0) - { - static::registerModelEvent('saving', $callback, $priority); - } - - /** - * Register an updated model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - */ - public static function updated($callback, $priority = 0) - { - static::registerModelEvent('updated', $callback, $priority); - } - - /** - * Register a creating model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - */ - public static function creating($callback, $priority = 0) - { - static::registerModelEvent('creating', $callback, $priority); - } - - /** - * Register a created model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - */ - public static function created($callback, $priority = 0) - { - static::registerModelEvent('created', $callback, $priority); - } - - /** - * Register a deleting model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - */ - public static function deleting($callback, $priority = 0) - { - static::registerModelEvent('deleting', $callback, $priority); - } - - /** - * Register a deleted model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - */ - public static function deleted($callback, $priority = 0) - { - static::registerModelEvent('deleted', $callback, $priority); - } - - /** - * Register an updating model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - */ - public static function updating($callback, $priority = 0) - { - static::registerModelEvent('updating', $callback, $priority); - } - - /** - * Register a saved model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - */ - public static function saved($callback, $priority = 0) - { - static::registerModelEvent('saved', $callback, $priority); - } - - /** - * Remove all of the event listeners for the model. - */ - public static function flushEventListeners() - { - if (!isset(static::$dispatcher)) { - return; - } - - $instance = new static(); - - foreach ($instance->getObservableEvents() as $event) { - static::$dispatcher->forget("eloquent.{$event}: ".get_called_class()); - } - } - - /** - * Register a model event with the dispatcher. - * - * @param string $event - * @param \Closure|string $callback - * @param int $priority - */ - protected static function registerModelEvent($event, $callback, $priority = 0) - { - if (isset(static::$dispatcher)) { - $name = get_called_class(); - - static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback, $priority); - } - } - - /** - * Get the observable event names. - * - * @return array - */ - public function getObservableEvents() - { - return array_merge( - [ - 'creating', 'created', 'updating', 'updated', - 'deleting', 'deleted', 'saving', 'saved', - 'restoring', 'restored', - ], - $this->observables - ); - } - - /** - * Set the observable event names. - * - * @param array $observables - */ - public function setObservableEvents(array $observables) - { - $this->observables = $observables; - } - - /** - * Add an observable event name. - * - * @param mixed $observables - */ - public function addObservableEvents($observables) - { - $observables = is_array($observables) ? $observables : func_get_args(); - - $this->observables = array_unique(array_merge($this->observables, $observables)); - } - - /** - * Remove an observable event name. - * - * @param mixed $observables - */ - public function removeObservableEvents($observables) - { - $observables = is_array($observables) ? $observables : func_get_args(); - - $this->observables = array_diff($this->observables, $observables); - } - - /** - * Increment a column's value by a given amount. - * - * @param string $column - * @param int $amount - * - * @return int - */ - protected function increment($column, $amount = 1) - { - return $this->incrementOrDecrement($column, $amount, 'increment'); - } - - /** - * Decrement a column's value by a given amount. - * - * @param string $column - * @param int $amount - * - * @return int - */ - protected function decrement($column, $amount = 1) - { - return $this->incrementOrDecrement($column, $amount, 'decrement'); - } - - /** - * Run the increment or decrement method on the model. - * - * @param string $column - * @param int $amount - * @param string $method - * - * @return int - */ - protected function incrementOrDecrement($column, $amount, $method) - { - $query = $this->newQuery(); - - if (!$this->exists) { - return $query->{$method}($column, $amount); - } - - $this->incrementOrDecrementAttributeValue($column, $amount, $method); - - return $query->where($this->getKeyName(), $this->getKey())->{$method}($column, $amount); - } - - /** - * Increment the underlying attribute value and sync with original. - * - * @param string $column - * @param int $amount - * @param string $method - */ - protected function incrementOrDecrementAttributeValue($column, $amount, $method) - { - $this->{$column} = $this->{$column} + ($method == 'increment' ? $amount : $amount * -1); - - $this->syncOriginalAttribute($column); - } - - /** - * Update the model in the database. - * - * @param array $attributes - * - * @return bool|int - */ - public function update(array $attributes = []) - { - if (!$this->exists) { - return $this->newQuery()->update($attributes); - } - - return $this->fill($attributes)->save(); - } - - /** - * Save the model and all of its relationships. - * - * @return bool - */ - public function push() - { - if (!$this->save()) { - return false; - } - - // To sync all of the relationships to the database, we will simply spin through - // the relationships and save each model via this "push" method, which allows - // us to recurse into all of these nested relations for the model instance. - foreach ($this->relations as $models) { - $models = $models instanceof Collection - ? $models->all() : [$models]; - - foreach (array_filter($models) as $model) { - if (!$model->push()) { - return false; - } - } - } - - return true; - } - - /** - * Save the model to the database. - * - * @param array $options - * - * @return bool - */ - public function save(array $options = []) - { - $query = $this->newQueryWithoutScopes(); - - // If the "saving" event returns false we'll bail out of the save and return - // false, indicating that the save failed. This provides a chance for any - // listeners to cancel save operations if validations fail or whatever. - if ($this->fireModelEvent('saving') === false) { - return false; - } - - // If the model already exists in the database we can just update our record - // that is already in this database using the current IDs in this "where" - // clause to only update this model. Otherwise, we'll just insert them. - if ($this->exists) { - $saved = $this->performUpdate($query, $options); - } - - // If the model is brand new, we'll insert it into our database and set the - // ID attribute on the model to the value of the newly inserted row's ID - // which is typically an auto-increment value managed by the database. - else { - $saved = $this->performInsert($query, $options); - } - - if ($saved) { - $this->finishSave($options); - } - - return $saved; - } - - /** - * Finish processing on a successful save operation. - * - * @param array $options - */ - protected function finishSave(array $options) - { - $this->fireModelEvent('saved', false); - - $this->syncOriginal(); - - if (Arr::get($options, 'touch', true)) { - $this->touchOwners(); - } - } - - /** - * Perform a model update operation. - * - * @param \Vinelab\NeoEloquent\Eloquent\Builder $query - * @param array $options - * - * @return bool|null - */ - protected function performUpdate(EloquentBuilder $query, array $options = []) - { - $dirty = $this->getDirty(); - - if (count($dirty) > 0) { - // If the updating event returns false, we will cancel the update operation so - // developers can hook Validation systems into their models and cancel this - // operation if the model does not pass validation. Otherwise, we update. - if ($this->fireModelEvent('updating') === false) { - return false; - } - - // First we need to create a fresh query instance and touch the creation and - // update timestamp on the model which are maintained by us for developer - // convenience. Then we will just continue saving the model instances. - if ($this->timestamps && Arr::get($options, 'timestamps', true)) { - $this->updateTimestamps(); - } - - // Once we have run the update operation, we will fire the "updated" event for - // this model instance. This will allow developers to hook into these after - // models are updated, giving them a chance to do any special processing. - $dirty = $this->getDirty(); - - if (count($dirty) > 0) { - $this->setKeysForSaveQuery($query)->update($dirty); - - $this->fireModelEvent('updated', false); - } - } - - return true; - } - - /** - * Perform a model insert operation. - * - * @param \Vinelab\NeoEloquent\Eloquent\Builder $query - * @param array $options - * - * @return bool - */ - protected function performInsert(EloquentBuilder $query, array $options = []) - { - if ($this->fireModelEvent('creating') === false) { - return false; - } - - // First we'll need to create a fresh query instance and touch the creation and - // update timestamps on this model, which are maintained by us for developer - // convenience. After, we will just continue saving these model instances. - if ($this->timestamps && Arr::get($options, 'timestamps', true)) { - $this->updateTimestamps(); - } - - // If the model has an incrementing key, we can use the "insertGetId" method on - // the query builder, which will give us back the final inserted ID for this - // table from the database. Not all tables have to be incrementing though. - $attributes = $this->attributes; - - if ($this->incrementing) { - $this->insertAndSetId($query, $attributes); - } - - // If the table is not incrementing we'll simply insert this attributes as they - // are, as this attributes arrays must contain an "id" column already placed - // there by the developer as the manually determined key for these models. - else { - $query->insert($attributes); - } - - // We will go ahead and set the exists property to true, so that it is set when - // the created event is fired, just in case the developer tries to update it - // during the event. This will allow them to do so and run an update here. - $this->exists = true; - - $this->wasRecentlyCreated = true; - - $this->fireModelEvent('created', false); - - return true; - } - - /** - * Insert the given attributes and set the ID on the model. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param array $attributes - */ - protected function insertAndSetId(Builder $query, $attributes) - { - $id = $query->insertGetId($attributes, $keyName = $this->getKeyName()); - - $this->setAttribute($keyName, $attributes[$this->getKeyName()] ?? $id); - } - - /** - * Touch the owning relations of the model. - */ - public function touchOwners() - { - foreach ($this->touches as $relation) { - $this->$relation()->touch(); - - if ($this->$relation instanceof self) { - $this->$relation->touchOwners(); - } elseif ($this->$relation instanceof Collection) { - $this->$relation->each(function (Model $relation) { - $relation->touchOwners(); - }); - } - } - } - - /** - * Determine if the model touches a given relation. - * - * @param string $relation - * - * @return bool - */ - public function touches($relation) - { - return in_array($relation, $this->touches); - } - - /** - * Fire the given event for the model. - * - * @param string $event - * @param bool $halt - * - * @return mixed - */ - protected function fireModelEvent($event, $halt = true) - { - if (!isset(static::$dispatcher)) { - return true; - } - - // We will append the names of the class to the event to distinguish it from - // other model events that are fired, allowing us to listen on each model - // event set individually instead of catching event for all the models. - $event = "eloquent.{$event}: ".get_class($this); - - $method = $halt ? 'until' : 'dispatch'; - - return static::$dispatcher->$method($event, $this); - } - - /** - * Set the keys for a save update query. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * - * @return \Illuminate\Database\Eloquent\Builder - */ - protected function setKeysForSaveQuery(Builder $query) - { - $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery()); - - return $query; - } - - /** - * Get the primary key value for a save query. - * - * @return mixed - */ - protected function getKeyForSaveQuery() - { - if (isset($this->original[$this->getKeyName()])) { - return $this->original[$this->getKeyName()]; - } - - return $this->getAttribute($this->getKeyName()); - } - - /** - * Update the model's update timestamp. - * - * @return bool - */ - public function touch() - { - if (!$this->timestamps) { - return false; - } - - $this->updateTimestamps(); - - return $this->save(); - } - - /** - * Update the creation and update timestamps. - */ - protected function updateTimestamps() - { - $time = $this->freshTimestamp(); - - if (!$this->isDirty(static::UPDATED_AT)) { - $this->setUpdatedAt($time); - } - - if (!$this->exists && !$this->isDirty(static::CREATED_AT)) { - $this->setCreatedAt($time); - } - } - - /** - * Set the value of the "created at" attribute. - * - * @param mixed $value - */ - public function setCreatedAt($value) - { - $this->{static::CREATED_AT} = $value; - } - - /** - * Set the value of the "updated at" attribute. - * - * @param mixed $value - */ - public function setUpdatedAt($value) - { - $this->{static::UPDATED_AT} = $value; - } - - /** - * Get the name of the "created at" column. - * - * @return string - */ - public function getCreatedAtColumn() - { - return static::CREATED_AT; - } - - /** - * Get the name of the "updated at" column. - * - * @return string - */ - public function getUpdatedAtColumn() - { - return static::UPDATED_AT; - } - - /** - * Get a fresh timestamp for the model. - * - * @return \Carbon\Carbon - */ - public function freshTimestamp() - { - return new Carbon(); - } - - /** - * Get a fresh timestamp for the model. - * - * @return string - */ - public function freshTimestampString() - { - return $this->fromDateTime($this->freshTimestamp()); - } - - /** - * Get a new query builder for the model's table. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function newQuery() - { - $builder = $this->newQueryWithoutScopes(); - - return $this->applyGlobalScopes($builder); - } - - /** - * Get a new query instance without a given scope. - * - * @param \Illuminate\Database\Eloquent\ScopeInterface $scope - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function newQueryWithoutScope($scope) - { - $this->getGlobalScope($scope)->remove($builder = $this->newQuery(), $this); - - return $builder; - } - - /** - * Get a new query builder that doesn't have any global scopes. - * - * @return \Illuminate\Database\Eloquent\Builder|static - */ - public function newQueryWithoutScopes() - { - $builder = $this->newEloquentBuilder( - $this->newBaseQueryBuilder() - ); - - // Once we have the query builders, we will set the model instances so the - // builder can easily access any information it may need from the model - // while it is constructing and executing various queries against it. - return $builder->setModel($this)->with($this->with); - } - - /** - * Apply all of the global scopes to an Eloquent builder. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function applyGlobalScopes($builder) - { - foreach ($this->getGlobalScopes() as $scope) { - $scope->apply($builder, $this); - } - - return $builder; - } - - /** - * Remove all of the global scopes from an Eloquent builder. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function removeGlobalScopes($builder) - { - foreach ($this->getGlobalScopes() as $scope) { - $scope->remove($builder, $this); - } - - return $builder; - } - - /** - * Create a new Eloquent Collection instance. - * - * @param array $models - * - * @return \Illuminate\Database\Eloquent\Collection - */ - public function newCollection(array $models = []) - { - return new Collection($models); - } - - /** - * Get the value of the model's primary key. - * - * @return mixed - */ - public function getKey() - { - return $this->getAttribute($this->getKeyName()); - } - - /** - * Get the queueable identity for the entity. - * - * @return mixed - */ - public function getQueueableId() - { - return $this->getKey(); - } - - /** - * Get the primary key for the model. - * - * @return string - */ - public function getKeyName() - { - return $this->primaryKey; - } - - /** - * Set the primary key for the model. - * - * @param string $key - */ - public function setKeyName($key) - { - $this->primaryKey = $key; - } - - /** - * Get the value of the model's route key. - * - * @return mixed - */ - public function getRouteKey() - { - return $this->getAttribute($this->getRouteKeyName()); - } - - /** - * Get the route key for the model. - * - * @return string - */ - public function getRouteKeyName() - { - return $this->getKeyName(); - } - - /** - * Determine if the model uses timestamps. - * - * @return bool - */ - public function usesTimestamps() - { - return $this->timestamps; - } - - /** - * Create a model with its relations. - * - * @param array $attributes - * @param array $relations - * @param array $options - * - * @return \Vinelab\NeoEloquent\Eloquent\Model - */ - public static function createWith(array $attributes, array $relations, array $options = []) - { - // we need to fire model events on all the models that are involved with our operaiton, - // including the ones from the relations, starting with this model. - $me = new static(); - $me->fill($attributes); - $models = [$me]; - - $query = static::query(); - $grammar = $query->getQuery()->getGrammar(); - - // add parent model's mutation constraints - $label = $grammar->modelAsNode($me->getDefaultNodeLabel()); - $query->addManyMutation($label, $me); - - // setup relations - foreach ($relations as $relation => $values) { - $related = $me->$relation()->getRelated(); - - // if the relation holds the attributes directly instead of an array - // of attributes, we transform it into an array of attributes. - if ((!is_array($values) || Helpers::isAssocArray($values)) && !$values instanceof Collection) { - $values = [$values]; - } - - // create instances with the related attributes so that we fire model - // events on each of them. - foreach ($values as $relatedModel) { - // one may pass in either instances or arrays of attributes, when we get - // attributes we will dynamically fill a new model instance of the related model. - if (is_array($relatedModel)) { - $model = $related->newInstance(); - $model->fill($relatedModel); - $relatedModel = $model; - } - - $models[$relation][] = $relatedModel; - $query->addManyMutation($relation, $related); - } - } - - $existingModelsKeys = []; - // fire 'creating' and 'saving' events on all models. - foreach ($models as $relation => $related) { - if (!is_array($related)) { - $related = [$related]; - } - - foreach ($related as $model) { - // we will fire model events on actual models, however attached models using IDs will not be considered. - if ($model instanceof Model) { - if (!$model->exists && $model->fireModelEvent('creating') === false) { - return false; - } - - if($model->exists) { - $existingModelsKeys[] = $model->getKey(); - } - - if ($model->fireModelEvent('saving') === false) { - return false; - } - } else { - $existingModelsKeys[] = $model; - } - } - } - - // remove $me from $models so that we send them as relations. - array_shift($models); - // run the query and create the records. - $result = $query->createWith($me->toArray(), $models); - // take the parent model that was created out of the results array based on - // this model's label. - $created = reset($result[$label]); - // fire 'saved' and 'created' events on parent model. - $created->finishSave($options); - $created->fireModelEvent('created', false); - - // set related models as relations on the parent model. - foreach ($relations as $method => $values) { - $relation = $created->$method(); - // is this a one-to-one relation ? If so then we add the model directly, - // otherwise we create a collection of the loaded models. - $related = new Collection($result[$method]); - // fire model events 'created' and 'saved' on related models. - $related->each(function ($model) use ($options, $existingModelsKeys) { - $model->finishSave($options); - // var_dump(get_class($model), 'saved'); - - if(!in_array($model->getKey(), $existingModelsKeys)) { - $model->fireModelEvent('created', false); - } - }); - - // when the relation is 'One' instead of 'Many' we will only return the retrieved instance - // instead of colletion. - if ($relation instanceof OneRelation || $relation instanceof HasOne || $relation instanceof BelongsTo) { - $related = $related->first(); - } - - $created->setRelation($method, $related); - } - - return $created; - } - /** - * Get the polymorphic relationship columns. - * - * @param string $name - * @param string $type - * @param string $id - * - * @return array - */ - protected function getMorphs($name, $type, $id) - { - $type = $type ?: $name.'_type'; - - $id = $this->getkeyname(); - - return array($type, $id); - } - - /** - * Get the class name for polymorphic relations. - * - * @return string - */ - public function getMorphClass() - { - $morphMap = Relation::morphMap(); - - $class = get_class($this); - - if (!empty($morphMap)) { - return array_search($class, $morphMap, true); - } - - return $this->morphClass ?: $class; - } - - /** - * Get the number of models to return per page. - * - * @return int - */ - public function getPerPage() - { - return $this->perPage; - } - - /** - * Set the number of models to return per page. - * - * @param int $perPage - */ - public function setPerPage($perPage) - { - $this->perPage = $perPage; - } - - /** - * Get the hidden attributes for the model. - * - * @return array - */ - public function getHidden() - { - return $this->hidden; - } - - /** - * Set the hidden attributes for the model. - * - * @param array $hidden - */ - public function setHidden(array $hidden) - { - $this->hidden = $hidden; - } - - /** - * Add hidden attributes for the model. - * - * @param array|string|null $attributes - */ - public function addHidden($attributes = null) - { - $attributes = is_array($attributes) ? $attributes : func_get_args(); - - $this->hidden = array_merge($this->hidden, $attributes); - } - - /** - * Make the given, typically hidden, attributes visible. - * - * @param array|string $attributes - * - * @return $this - */ - public function withHidden($attributes) - { - $this->hidden = array_diff($this->hidden, (array) $attributes); - - return $this; - } - - /** - * Get the visible attributes for the model. - * - * @return array - */ - public function getVisible() - { - return $this->visible; - } - - /** - * Set the visible attributes for the model. - * - * @param array $visible - */ - public function setVisible(array $visible) - { - $this->visible = $visible; - } - - /** - * Add visible attributes for the model. - * - * @param array|string|null $attributes - */ - public function addVisible($attributes = null) - { - $attributes = is_array($attributes) ? $attributes : func_get_args(); - - $this->visible = array_merge($this->visible, $attributes); - } - - /** - * Set the accessors to append to model arrays. - * - * @param array $appends - */ - public function setAppends(array $appends) - { - $this->appends = $appends; - } - - /** - * Get the fillable attributes for the model. - * - * @return array - */ - public function getFillable() - { - return $this->fillable; - } - - /** - * Set the fillable attributes for the model. - * - * @param array $fillable - * - * @return $this - */ - public function fillable(array $fillable) - { - $this->fillable = $fillable; - - return $this; - } - - /** - * Get the guarded attributes for the model. - * - * @return array - */ - public function getGuarded() - { - return $this->guarded; - } - - /** - * Set the guarded attributes for the model. - * - * @param array $guarded - * - * @return $this - */ - public function guard(array $guarded) - { - $this->guarded = $guarded; - - return $this; - } - - /** - * Disable all mass assignable restrictions. - * - * @param bool $state - */ - public static function unguard($state = true) - { - static::$unguarded = $state; - } - - /** - * Enable the mass assignment restrictions. - */ - public static function reguard() - { - static::$unguarded = false; - } - - /** - * Determine if current state is "unguarded". - * - * @return bool - */ - public static function isUnguarded() - { - return static::$unguarded; - } - - /** - * Run the given callable while being unguarded. - * - * @param callable $callback - * - * @return mixed - */ - public static function unguarded(callable $callback) - { - if (static::$unguarded) { - return $callback(); - } - - static::unguard(); - - $result = $callback(); - - static::reguard(); - - return $result; - } - - /** - * Determine if the given attribute may be mass assigned. - * - * @param string $key - * - * @return bool - */ - public function isFillable($key) - { - if (static::$unguarded) { - return true; - } - - // If the key is in the "fillable" array, we can of course assume that it's - // a fillable attribute. Otherwise, we will check the guarded array when - // we need to determine if the attribute is black-listed on the model. - if (in_array($key, $this->fillable)) { - return true; - } - - if ($this->isGuarded($key)) { - return false; - } - - return empty($this->fillable) && !Str::startsWith($key, '_'); - } - - /** - * Determine if the given key is guarded. - * - * @param string $key - * - * @return bool - */ - public function isGuarded($key) - { - return in_array($key, $this->guarded) || $this->guarded == ['*']; - } - - /** - * Determine if the model is totally guarded. - * - * @return bool - */ - public function totallyGuarded() - { - return count($this->fillable) == 0 && $this->guarded == ['*']; - } - - /** - * Get the relationships that are touched on save. - * - * @return array - */ - public function getTouchedRelations() - { - return $this->touches; - } - - /** - * Set the relationships that are touched on save. - * - * @param array $touches - */ - public function setTouchedRelations(array $touches) - { - $this->touches = $touches; - } - - /** - * Get the value indicating whether the IDs are incrementing. - * - * @return bool - */ - public function getIncrementing() - { - return $this->incrementing; - } - - /** - * Set whether IDs are incrementing. - * - * @param bool $value - */ - public function setIncrementing($value) - { - $this->incrementing = $value; - } - - /** - * Convert the model instance to JSON. - * - * @param int $options - * - * @return string - */ - public function toJson($options = 0) - { - return json_encode($this->jsonSerialize(), $options); - } - - /** - * Convert the object into something JSON serializable. - * - * @return array - */ - public function jsonSerialize() - { - return $this->toArray(); - } - - /** - * Convert the model instance to an array. - * - * @return array - */ - public function toArray() - { - $attributes = $this->attributesToArray(); - - return array_merge($attributes, $this->relationsToArray()); - } - - /** - * Convert the model's attributes to an array. - * - * @return array - */ - public function attributesToArray() - { - $attributes = $this->getArrayableAttributes(); - - // If an attribute is a date, we will cast it to a string after converting it - // to a DateTime / Carbon instance. This is so we will get some consistent - // formatting while accessing attributes vs. arraying / JSONing a model. - foreach ($this->getDates() as $key) { - if (!isset($attributes[$key])) { - continue; - } - - $attributes[$key] = $this->serializeDate( - $this->asDateTime($attributes[$key]) - ); - } - - $mutatedAttributes = $this->getMutatedAttributes(); - - // We want to spin through all the mutated attributes for this model and call - // the mutator for the attribute. We cache off every mutated attributes so - // we don't have to constantly check on attributes that actually change. - foreach ($mutatedAttributes as $key) { - if (!array_key_exists($key, $attributes)) { - continue; - } - - $attributes[$key] = $this->mutateAttributeForArray( - $key, $attributes[$key] - ); - } - - // Next we will handle any casts that have been setup for this model and cast - // the values to their appropriate type. If the attribute has a mutator we - // will not perform the cast on those attributes to avoid any confusion. - foreach ($this->casts as $key => $value) { - if (!array_key_exists($key, $attributes) || - in_array($key, $mutatedAttributes)) { - continue; - } - - $attributes[$key] = $this->castAttribute( - $key, $attributes[$key] - ); - } - - // Here we will grab all of the appended, calculated attributes to this model - // as these attributes are not really in the attributes array, but are run - // when we need to array or JSON the model for convenience to the coder. - foreach ($this->getArrayableAppends() as $key) { - $attributes[$key] = $this->mutateAttributeForArray($key, null); - } - - return $attributes; - } - - /** - * Get an attribute array of all arrayable attributes. - * - * @return array - */ - protected function getArrayableAttributes() - { - return $this->getArrayableItems($this->attributes); - } - - /** - * Get all of the appendable values that are arrayable. - * - * @return array - */ - protected function getArrayableAppends() - { - if (!count($this->appends)) { - return []; - } - - return $this->getArrayableItems( - array_combine($this->appends, $this->appends) - ); - } - - /** - * Get the model's relationships in array form. - * - * @return array - */ - public function relationsToArray() - { - $attributes = []; - - foreach ($this->getArrayableRelations() as $key => $value) { - // If the values implements the Arrayable interface we can just call this - // toArray method on the instances which will convert both models and - // collections to their proper array form and we'll set the values. - if ($value instanceof Arrayable) { - $relation = $value->toArray(); - } - - // If the value is null, we'll still go ahead and set it in this list of - // attributes since null is used to represent empty relationships if - // if it a has one or belongs to type relationships on the models. - elseif (is_null($value)) { - $relation = $value; - } - - // If the relationships snake-casing is enabled, we will snake case this - // key so that the relation attribute is snake cased in this returned - // array to the developers, making this consistent with attributes. - if (static::$snakeAttributes) { - $key = Str::snake($key); - } - - // If the relation value has been set, we will set it on this attributes - // list for returning. If it was not arrayable or null, we'll not set - // the value on the array because it is some type of invalid value. - if (isset($relation) || is_null($value)) { - $attributes[$key] = $relation; - } - - unset($relation); - } - - return $attributes; - } - - /** - * Get an attribute array of all arrayable relations. - * - * @return array - */ - protected function getArrayableRelations() - { - return $this->getArrayableItems($this->relations); - } - - /** - * Get an attribute array of all arrayable values. - * - * @param array $values - * - * @return array - */ - protected function getArrayableItems(array $values) - { - if (count($this->getVisible()) > 0) { - return array_intersect_key($values, array_flip($this->getVisible())); - } - - return array_diff_key($values, array_flip($this->getHidden())); - } - - /** - * Get a plain attribute (not a relationship). - * - * @param string $key - * - * @return mixed - */ - public function getAttributeValue($key) - { - $value = $this->getAttributeFromArray($key); - - // If the attribute has a get mutator, we will call that then return what - // it returns as the value, which is useful for transforming values on - // retrieval from the model to a form that is more useful for usage. - if ($this->hasGetMutator($key)) { - return $this->mutateAttribute($key, $value); - } - - // If the attribute exists within the cast array, we will convert it to - // an appropriate native PHP type dependant upon the associated value - // given with the key in the pair. Dayle made this comment line up. - if ($this->hasCast($key)) { - $value = $this->castAttribute($key, $value); - } - - // If the attribute is listed as a date, we will convert it to a DateTime - // instance on retrieval, which makes it quite convenient to work with - // date fields without having to create a mutator for each property. - elseif (in_array($key, $this->getDates())) { - if (!is_null($value)) { - return $this->asDateTime($value); - } - } - - return $value; - } - - /** - * Get an attribute from the model. - * - * @param string $key - * - * @return mixed - */ - public function getAttribute($key) - { - if (array_key_exists($key, $this->attributes) || $this->hasGetMutator($key)) { - return $this->getAttributeValue($key); - } - - return $this->getRelationValue($key); - } - - /** - * Get a relationship. - * - * @param string $key - * - * @return mixed - */ - public function getRelationValue($key) - { - // If the key already exists in the relationships array, it just means the - // relationship has already been loaded, so we'll just return it out of - // here because there is no need to query within the relations twice. - if ($this->relationLoaded($key)) { - return $this->relations[$key]; - } - - // If the "attribute" exists as a method on the model, we will just assume - // it is a relationship and will load and return results from the query - // and hydrate the relationship's value on the "relationships" array. - if (method_exists($this, $key)) { - return $this->getRelationshipFromMethod($key); - } - } - - /** - * Get an attribute from the $attributes array. - * - * @param string $key - * - * @return mixed - */ - protected function getAttributeFromArray($key) - { - if (array_key_exists($key, $this->attributes)) { - return $this->attributes[$key]; - } - } - - /** - * Get a relationship value from a method. - * - * @param string $method - * - * @return mixed - * - * @throws \LogicException - */ - protected function getRelationshipFromMethod($method) - { - $relations = $this->$method(); - - if (!$relations instanceof Relation) { - throw new LogicException('Relationship method must return an object of type ' - .'Vinelab\NeoEloquent\Eloquent\Relations\Relation'); - } - - return $this->relations[$method] = $relations->getResults(); - } - - /** - * Determine if a get mutator exists for an attribute. - * - * @param string $key - * - * @return bool - */ - public function hasGetMutator($key) - { - return method_exists($this, 'get'.Str::studly($key).'Attribute'); - } - - /** - * Get the value of an attribute using its mutator. - * - * @param string $key - * @param mixed $value - * - * @return mixed - */ - protected function mutateAttribute($key, $value) - { - return $this->{'get'.Str::studly($key).'Attribute'}($value); - } - - /** - * Get the value of an attribute using its mutator for array conversion. - * - * @param string $key - * @param mixed $value - * - * @return mixed - */ - protected function mutateAttributeForArray($key, $value) - { - $value = $this->mutateAttribute($key, $value); - - return $value instanceof Arrayable ? $value->toArray() : $value; - } - - /** - * Determine whether an attribute should be casted to a native type. - * - * @param string $key - * - * @return bool - */ - protected function hasCast($key) - { - return array_key_exists($key, $this->casts); - } - - /** - * Determine whether a value is JSON castable for inbound manipulation. - * - * @param string $key - * - * @return bool - */ - protected function isJsonCastable($key) - { - if ($this->hasCast($key)) { - return in_array( - $this->getCastType($key), ['array', 'json', 'object', 'collection'], true - ); - } - - return false; - } - - /** - * Get the type of cast for a model attribute. - * - * @param string $key - * - * @return string - */ - protected function getCastType($key) - { - return trim(strtolower($this->casts[$key])); - } - - /** - * Cast an attribute to a native PHP type. - * - * @param string $key - * @param mixed $value - * - * @return mixed - */ - protected function castAttribute($key, $value) - { - if (is_null($value)) { - return $value; - } - - switch ($this->getCastType($key)) { - case 'int': - case 'integer': - return (int) $value; - case 'real': - case 'float': - case 'double': - return (float) $value; - case 'string': - return (string) $value; - case 'bool': - case 'boolean': - return (bool) $value; - case 'object': - return json_decode($value); - case 'array': - case 'json': - return json_decode($value, true); - case 'collection': - return new BaseCollection(json_decode($value, true)); - default: - return $value; - } - } - - /** - * Set a given attribute on the model. - * - * @param string $key - * @param mixed $value - */ - public function setAttribute($key, $value) - { - // First we will check for the presence of a mutator for the set operation - // which simply lets the developers tweak the attribute as it is set on - // the model, such as "json_encoding" an listing of data for storage. - if ($this->hasSetMutator($key)) { - $method = 'set'.Str::studly($key).'Attribute'; - - return $this->{$method}($value); - } - - // If an attribute is listed as a "date", we'll convert it from a DateTime - // instance into a form proper for storage on the database tables using - // the connection grammar's date format. We will auto set the values. - elseif (in_array($key, $this->getDates()) && $value) { - $value = $this->fromDateTime($value); - } - - if ($this->isJsonCastable($key) && !is_null($value)) { - $value = json_encode($value); - } - - $this->attributes[$key] = $value; - } - - /** - * Determine if a set mutator exists for an attribute. - * - * @param string $key - * - * @return bool - */ - public function hasSetMutator($key) - { - return method_exists($this, 'set'.Str::studly($key).'Attribute'); - } - - /** - * Get the attributes that should be converted to dates. - * - * @return array - */ - public function getDates() - { - $defaults = [static::CREATED_AT, static::UPDATED_AT]; - - return $this->timestamps ? array_merge($this->dates, $defaults) : $this->dates; - } - - /** - * Convert a DateTime to a storable string. - * - * @param \DateTime|int $value - * - * @return string - */ - public function fromDateTime($value) - { - $format = $this->getDateFormat(); - - $value = $this->asDateTime($value); - - return $value->format($format); - } - - /** - * Return a timestamp as DateTime object. - * - * @param mixed $value - * - * @return \Carbon\Carbon - */ - protected function asDateTime($value) - { - // If this value is already a Carbon instance, we shall just return it as is. - // This prevents us having to reinstantiate a Carbon instance when we know - // it already is one, which wouldn't be fulfilled by the DateTime check. - if ($value instanceof Carbon) { - return $value; - } - - // If the value is already a DateTime instance, we will just skip the rest of - // these checks since they will be a waste of time, and hinder performance - // when checking the field. We will just return the DateTime right away. - if ($value instanceof DateTime) { - return Carbon::instance($value); - } - - // If this value is an integer, we will assume it is a UNIX timestamp's value - // and format a Carbon object from this timestamp. This allows flexibility - // when defining your date fields as they might be UNIX timestamps here. - if (is_numeric($value)) { - return Carbon::createFromTimestamp($value); - } - - // If the value is in simply year, month, day format, we will instantiate the - // Carbon instances from that format. Again, this provides for simple date - // fields on the database, while still supporting Carbonized conversion. - if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value)) { - return Carbon::createFromFormat('Y-m-d', $value)->startOfDay(); - } - - // Finally, we will just assume this date is in the format used by default on - // the database connection and use that format to create the Carbon object - // that is returned back out to the developers after we convert it here. - return Carbon::createFromFormat($this->getDateFormat(), $value); - } - - /** - * Prepare a date for array / JSON serialization. - * - * @param \DateTime $date - * - * @return string - */ - protected function serializeDate(DateTime $date) - { - return $date->format($this->getDateFormat()); - } - - /** - * Get the format for database stored dates. - * - * @return string - */ - protected function getDateFormat() - { - return $this->dateFormat ?: $this->getConnection()->getQueryGrammar()->getDateFormat(); - } - - /** - * Set the date format used by the model. - * - * @param string $format - * - * @return $this - */ - public function setDateFormat($format) - { - $this->dateFormat = $format; - - return $this; - } - - /** - * Clone the model into a new, non-existing instance. - * - * @param array $except - * - * @return \Illuminate\Database\Eloquent\Model - */ - public function replicate(array $except = null) - { - $except = $except ?: [ - $this->getKeyName(), - $this->getCreatedAtColumn(), - $this->getUpdatedAtColumn(), - ]; - - $attributes = array_except($this->attributes, $except); - - with($instance = new static())->setRawAttributes($attributes); - - return $instance->setRelations($this->relations); - } - - /** - * Get all of the current attributes on the model. - * - * @return array - */ - public function getAttributes() - { - return $this->attributes; - } - - /** - * Set the array of model attributes. No checking is done. - * - * @param array $attributes - * @param bool $sync - */ - public function setRawAttributes(array $attributes, $sync = false) - { - $this->attributes = $attributes; - - if ($sync) { - $this->syncOriginal(); - } - } - - /** - * Get the model's original attribute values. - * - * @param string $key - * @param mixed $default - * - * @return array - */ - public function getOriginal($key = null, $default = null) - { - return Arr::get($this->original, $key, $default); - } - - /** - * Sync the original attributes with the current. - * - * @return $this - */ - public function syncOriginal() - { - $this->original = $this->attributes; - - return $this; - } - - /** - * Sync a single original attribute with its current value. - * - * @param string $attribute - * - * @return $this - */ - public function syncOriginalAttribute($attribute) - { - $this->original[$attribute] = $this->attributes[$attribute]; - - return $this; - } - - /** - * Determine if the model or given attribute(s) have been modified. - * - * @param array|string|null $attributes - * - * @return bool - */ - public function isDirty($attributes = null) - { - $dirty = $this->getDirty(); - - if (is_null($attributes)) { - return count($dirty) > 0; - } - - if (!is_array($attributes)) { - $attributes = func_get_args(); - } - - foreach ($attributes as $attribute) { - if (array_key_exists($attribute, $dirty)) { - return true; - } - } - - return false; - } - - /** - * Get the attributes that have been changed since last sync. - * - * @return array - */ - public function getDirty() - { - $dirty = []; - - foreach ($this->attributes as $key => $value) { - if (!array_key_exists($key, $this->original)) { - $dirty[$key] = $value; - } elseif ($value !== $this->original[$key] && - !$this->originalIsNumericallyEquivalent($key)) { - $dirty[$key] = $value; - } - } - - // We need to remove the primary key from the dirty attributes since primary keys - // never change and when updating it shouldn't be part of the attribtues. - if (isset($dirty[$this->primaryKey])) { - unset($dirty[$this->primaryKey]); - } - - return $dirty; - } - - /** - * Determine if the new and old values for a given key are numerically equivalent. - * - * @param string $key - * - * @return bool - */ - protected function originalIsNumericallyEquivalent($key) - { - $current = $this->attributes[$key]; - - $original = $this->original[$key]; - - return is_numeric($current) && is_numeric($original) && strcmp((string) $current, (string) $original) === 0; - } - - /** - * Get all the loaded relations for the instance. - * - * @return array - */ - public function getRelations() - { - return $this->relations; - } - - /** - * Get a specified relationship. - * - * @param string $relation - * - * @return mixed - */ - public function getRelation($relation) - { - return $this->relations[$relation]; - } - - /** - * Determine if the given relation is loaded. - * - * @param string $key - * - * @return bool - */ - public function relationLoaded($key) - { - return array_key_exists($key, $this->relations); - } - - /** - * Set the specific relationship in the model. - * - * @param string $relation - * @param mixed $value - * - * @return $this - */ - public function setRelation($relation, $value) - { - $this->relations[$relation] = $value; - - return $this; - } - - /** - * Determine whether a relationship exists on this model. - * - * @param string $relation - * - * @return boolean - */ - public function hasRelation($relation) - { - return isset($this->relations[$relation]); - } - - /** - * Set the entire relations array on the model. - * - * @param array $relations - * - * @return $this - */ - public function setRelations(array $relations) - { - $this->relations = $relations; - - return $this; - } - - /** - * Get the database connection for the model. - * - * @return \Illuminate\Database\Connection - */ - public function getConnection() - { - return static::resolveConnection($this->connection); - } - - /** - * Get the current connection name for the model. - * - * @return string - */ - public function getConnectionName() - { - return $this->connection; - } - - /** - * Set the connection associated with the model. - * - * @param string $name - * - * @return $this - */ - public function setConnection($name) - { - $this->connection = $name; - - return $this; - } - - /** - * Resolve a connection instance. - * - * @param string $connection - * - * @return \Illuminate\Database\Connection - */ - public static function resolveConnection($connection = null) - { - return static::$resolver->connection($connection); - } - - /** - * Get the connection resolver instance. - * - * @return \Illuminate\Database\ConnectionResolverInterface - */ - public static function getConnectionResolver() - { - return static::$resolver; - } - - /** - * Set the connection resolver instance. - * - * @param \Illuminate\Database\ConnectionResolverInterface $resolver - */ - public static function setConnectionResolver(Resolver $resolver) - { - static::$resolver = $resolver; - } - - /** - * Unset the connection resolver for models. - */ - public static function unsetConnectionResolver() - { - static::$resolver = null; - } - - /** - * Get the event dispatcher instance. - * - * @return \Illuminate\Contracts\Events\Dispatcher - */ - public static function getEventDispatcher() - { - return static::$dispatcher; - } - - /** - * Unset the event dispatcher for models. - */ - public static function unsetEventDispatcher() - { - static::$dispatcher = null; - } - - /** - * Get the mutated attributes for a given instance. - * - * @return array - */ - public function getMutatedAttributes() - { - $class = get_class($this); - - if (!isset(static::$mutatorCache[$class])) { - static::cacheMutatedAttributes($class); - } - - return static::$mutatorCache[$class]; - } - - /** - * Extract and cache all the mutated attributes of a class. - * - * @param string $class - */ - public static function cacheMutatedAttributes($class) - { - $mutatedAttributes = []; - - // Here we will extract all of the mutated attributes so that we can quickly - // spin through them after we export models to their array form, which we - // need to be fast. This'll let us know the attributes that can mutate. - foreach (get_class_methods($class) as $method) { - if (strpos($method, 'Attribute') !== false && - preg_match('/^get(.+)Attribute$/', $method, $matches)) { - if (static::$snakeAttributes) { - $matches[1] = Str::snake($matches[1]); - } - - $mutatedAttributes[] = lcfirst($matches[1]); - } - } - - static::$mutatorCache[$class] = $mutatedAttributes; - } - - /** - * Set the event dispatcher instance. - * - * @param \Illuminate\Contracts\Events\Dispatcher $dispatcher - */ - public static function setEventDispatcher(Dispatcher $dispatcher) - { - static::$dispatcher = $dispatcher; - } - - /** - * @override - * Get the table qualified key name. - * - * @return string - */ - public function getQualifiedKeyName() - { - return $this->getKeyName(); - } - - /** - * Add timestamps to this model. - */ - public function addTimestamps() - { - $this->updateTimestamps(); - } - - /* - * Adds more labels - * @param $labels array of strings containing labels to be added - * @return bull true if success, false if failure - */ - public function addLabels($labels) - { - return $this->updateLabels($labels, 'add'); - } - - /* - * Drops labels - * @param $labels array of strings containing labels to be dropped - * @return bull true if success, false if failure - */ - public function dropLabels($labels) - { - return $this->updateLabels($labels, 'drop'); - } - - /* - * Adds or Drops labels - * @param $labels array of strings containing labels to be dropped - * @param $operation string can be 'add' or 'drop' - * @return bull true if success, false if failure - */ - public function updateLabels($labels, $operation = 'add') - { - $query = $this->newQueryWithoutScopes(); - - // If the "saving" event returns false we'll bail out of the save and return - // false, indicating that the save failed. This gives an opportunities to - // listeners to cancel save operations if validations fail or whatever. - if ($this->fireModelEvent('saving') === false) { - return false; - } - - if (!is_array($labels) || count($labels) == 0) { - return false; - } - - foreach ($labels as $label) { - if (!preg_match('/^[a-z]([a-z0-9]+)$/i', $label)) { - return false; - } - } - - // If the model already exists in the database we can just update our record - // that is already in this database using the current IDs in this "where" - // clause to only update this model. Otherwise, we'll return false. - if ($this->exists) { - $this->setKeysForSaveQuery($query)->updateLabels($labels, $operation); - $this->fireModelEvent('updated', false); - } else { - return false; - } - } - - /** - * Dynamically retrieve attributes on the model. - * - * @param string $key - * - * @return mixed - */ - public function __get($key) - { - return $this->getAttribute($key); - } - - /** - * Dynamically set attributes on the model. - * - * @param string $key - * @param mixed $value - */ - public function __set($key, $value) - { - $this->setAttribute($key, $value); - } - - /** - * Determine if the given attribute exists. - * - * @param mixed $offset - * - * @return bool - */ - public function offsetExists($offset) - { - return isset($this->$offset); - } - - /** - * Get the value for a given offset. - * - * @param mixed $offset - * - * @return mixed - */ - public function offsetGet($offset) - { - return $this->$offset; - } - - /** - * Set the value for a given offset. - * - * @param mixed $offset - * @param mixed $value - */ - public function offsetSet($offset, $value) - { - $this->$offset = $value; - } - - /** - * Unset the value for a given offset. - * - * @param mixed $offset - */ - public function offsetUnset($offset) - { - unset($this->$offset); - } - - /** - * Determine if an attribute exists on the model. - * - * @param string $key - * - * @return bool - */ - public function __isset($key) - { - return (isset($this->attributes[$key]) || isset($this->relations[$key])) || - ($this->hasGetMutator($key) && !is_null($this->getAttributeValue($key))); - } - - /** - * Unset an attribute on the model. - * - * @param string $key - */ - public function __unset($key) - { - unset($this->attributes[$key], $this->relations[$key]); - } - - /** - * Handle dynamic method calls into the model. - * - * @param string $method - * @param array $parameters - * - * @return mixed - */ - public function __call($method, $parameters) - { - if (in_array($method, ['increment', 'decrement'])) { - return call_user_func_array([$this, $method], $parameters); - } - - $query = $this->newQuery(); - - return call_user_func_array([$query, $method], $parameters); - } - - /** - * Handle dynamic static method calls into the method. - * - * @param string $method - * @param array $parameters - * - * @return mixed - */ - public static function __callStatic($method, $parameters) - { - $instance = new static(); - - return call_user_func_array([$instance, $method], $parameters); - } - - /** - * Convert the model to its string representation. - * - * @return string - */ - public function __toString() - { - return $this->toJson(); - } - - /** - * When a model is being unserialized, check if it needs to be booted. - */ - public function __wakeup() - { - $this->bootIfNotBooted(); - } - - /** - * Get the queueable connection for the entity. - * - * @return mixed - */ - public function getQueueableConnection() - { - return $this->getConnectionName(); - } - - /** - * Retrieve the model for a bound value. - * - * @param mixed $value - * @return \Illuminate\Database\Eloquent\Model|null - */ - public function resolveRouteBinding($value, $field=null) - { - return $this->where($this->getRouteKeyName(), $value)->first(); - } - - public function getQueueableRelations() - { - throw new BadMethodCallException('NeoEloquent does not support queueable relations yet'); - } - - public function resolveChildRouteBinding($childType, $value, $field) - { - throw new BadMethodCallException('NeoEloquent does not support queueable relations yet'); - } -} diff --git a/src/Eloquent/NeoEloquentFactory.php b/src/Eloquent/NeoEloquentFactory.php deleted file mode 100644 index 6aee8665..00000000 --- a/src/Eloquent/NeoEloquentFactory.php +++ /dev/null @@ -1,240 +0,0 @@ - - */ -class NeoEloquentFactory implements ArrayAccess -{ - /** - * @var array - */ - protected $states = []; - - /** - * The Faker instance for the builder. - * - * @var \Faker\Generator - */ - protected $faker; - - /** - * Create a new factory instance. - * - * @param \Faker\Generator $faker - * @return void - */ - public function __construct(Faker $faker) - { - $this->faker = $faker; - } - - /** - * Create a new factory container. - * - * @param \Faker\Generator $faker - * @param string|null $pathToFactories - * @return static - */ - public static function construct(Faker $faker, $pathToFactories = null) - { - $pathToFactories = $pathToFactories ?: database_path('factories'); - - return (new static($faker))->load($pathToFactories); - } - - /** - * Define a class with a given short-name. - * - * @param string $class - * @param string $name - * @param callable $attributes - * @return $this - */ - public function defineAs($class, $name, callable $attributes) - { - return $this->define($class, $attributes, $name); - } - - /** - * Define a class with a given set of attributes. - * - * @param string $class - * @param callable $attributes - * @param string $name - * @return $this - */ - public function define($class, callable $attributes, $name = 'default') - { - $this->definitions[$class][$name] = $attributes; - - return $this; - } - - /** - * Define a state with a given set of attributes. - * - * @param string $class - * @param string $state - * @param callable|array $attributes - * @return $this - */ - public function state($class, $state, $attributes) - { - $this->states[$class][$state] = $attributes; - - return $this; - } - - /** - * Create a builder for the given model. - * - * @param $class - * @param string $name - * @return \Vinelab\NeoEloquent\Eloquent\NeoFactoryBuilder - */ - public function of($class, $name = 'default') - { - return new NeoFactoryBuilder($class, $name, $this->definitions, $this->states, $this->faker); - } - - /** - * Create an instance of the given model and type and persist it to the database. - * - * @param string $class - * @param string $name - * @param array $attributes - * @return mixed - */ - public function createAs($class, $name, array $attributes = []) - { - return $this->of($class, $name)->create($attributes); - } - - /** - * Create an instance of the given model. - * - * @param string $class - * @param array $attributes - * @return mixed - */ - public function make($class, array $attributes = []) - { - return $this->of($class)->make($attributes); - } - - /** - * Create an instance of the given model and type. - * - * @param string $class - * @param string $name - * @param array $attributes - * @return mixed - */ - public function makeAs($class, $name, array $attributes = []) - { - return $this->of($class, $name)->make($attributes); - } - - /** - * Get the raw attribute array for a given named model. - * - * @param string $class - * @param string $name - * @param array $attributes - * @return array - */ - public function rawOf($class, $name, array $attributes = []) - { - return $this->raw($class, $attributes, $name); - } - - /** - * Get the raw attribute array for a given model. - * - * @param string $class - * @param array $attributes - * @param string $name - * @return array - */ - public function raw($class, array $attributes = [], $name = 'default') - { - return array_merge( - call_user_func($this->definitions[$class][$name], $this->faker), - $attributes - ); - } - - /** - * Load factories from path. - * - * @param string $path - * @return $this - */ - public function load($path) - { - $factory = $this; - - if (is_dir($path)) { - foreach (Finder::create()->files()->name('*.php')->in($path) as $file) { - require $file->getRealPath(); - } - } - - return $factory; - } - - /** - * Determine if the given offset exists. - * - * @param string $offset - * @return bool - */ - public function offsetExists($offset) - { - return isset($this->definitions[$offset]); - } - - /** - * Get the value of the given offset. - * - * @param string $offset - * @return mixed - */ - public function offsetGet($offset) - { - return $this->make($offset); - } - - /** - * Set the given offset to the given value. - * - * @param string $offset - * @param callable $value - * @return void - */ - public function offsetSet($offset, $value) - { - return $this->define($offset, $value); - } - - /** - * Unset the value at the given offset. - * - * @param string $offset - * @return void - */ - public function offsetUnset($offset) - { - unset($this->definitions[$offset]); - } -} diff --git a/src/Eloquent/NeoFactoryBuilder.php b/src/Eloquent/NeoFactoryBuilder.php deleted file mode 100644 index 0fe1f038..00000000 --- a/src/Eloquent/NeoFactoryBuilder.php +++ /dev/null @@ -1,45 +0,0 @@ -make($attributes); - - if ($results instanceof Model) { - $this->store(collect([$results])); - } else { - $this->store($results); - } - - return $results; - } - - /** - * Set the connection name on the results and store them. - * - * @param \Illuminate\Support\Collection $results - * @return void - */ - protected function store($results) - { - $results->each(function ($model) { - if (! isset($this->connection)) { - - $model->setConnection($model->getConnectionName()); - } - - $model->save(); - }); - } -} diff --git a/src/Eloquent/Relations/BelongsTo.php b/src/Eloquent/Relations/BelongsTo.php deleted file mode 100644 index 0d3dddc1..00000000 --- a/src/Eloquent/Relations/BelongsTo.php +++ /dev/null @@ -1,116 +0,0 @@ -belongsTo('User', 'PHONE'); - * } - * } - */ - - // Get the parent node's placeholder. - $parentNode = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - // Tell the query that we only need the related model returned. - $this->query->select($this->relation); - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()<-[]-() Cypher clause. - $this->query->matchIn($this->parent, $this->related, $this->relation, $this->relationType, $this->otherKey, $this->parent->{$this->otherKey}); - // Add WHERE clause over the parent node's matching key = value. - $this->query->where($this->otherKey, '=', $this->parent->{$this->otherKey}); - } - } - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - */ - public function addEagerConstraints(array $models) - { - /* - * We'll grab the primary key name of the related models since it could be set to - * a non-standard name and not "id". We will then construct the constraint for - * our eagerly loading query so it returns the proper models from execution. - */ - - parent::addEagerConstraints($models); - - // Grab the parent node placeholder - $parentNode = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - - // Tell the builder to select both models of the relationship - $this->query->select($this->relation, $parentNode); - - // Setup for their mutation so they don't breed weird stuff like... humans ?! - $this->query->addMutation($this->relation, $this->related); - $this->query->addMutation($parentNode, $this->parent); - - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()<-[]-() Cypher clause. - $this->query->matchIn($this->parent, $this->related, $this->relation, $this->relationType, $this->otherKey, $this->parent->{$this->otherKey}); - // Add WHERE clause over the parent node's matching keys [values...]. - $this->query->whereIn($this->otherKey, $this->getEagerModelKeys($models)); - - $this->query->startModel = $this->parent; - $this->query->endModel = $this->related; - $this->query->relationshipName = $this->relation; - } - - /** - * Get an instance of the EdgeIn relationship. - * - * @param \Vinelab\NeoEloquent\Eloquent\Model $model - * @param array $attributes - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\EdgeIn - */ - public function getEdge(Model $model = null, $attributes = array()) - { - $model = (!is_null($model)) ? $model : $this->parent->{$this->relation}; - - // Indicate a unique relation since this only involves one other model. - $unique = true; - - return new EdgeIn($this->query, $this->parent, $model, $this->relationType, $attributes, $unique); - } -} diff --git a/src/Eloquent/Relations/BelongsToMany.php b/src/Eloquent/Relations/BelongsToMany.php deleted file mode 100644 index f2ea21ad..00000000 --- a/src/Eloquent/Relations/BelongsToMany.php +++ /dev/null @@ -1,177 +0,0 @@ -finder = $this->newFinder(); - } - - /** - * Get the results of the relationship. - * - * @return mixed - */ - public function getResults() - { - return $this->query->get(); - } - - /** - * Match the eagerly loaded results to their parents. - * - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $relation - * - * @return array - */ - public function match(array $models, Collection $results, $relation) - { - return $this->matchOneOrMany($models, $results, $relation, 'many'); - } - - /** - * Set the base constraints on the relation query. - */ - public function addConstraints() - { - if (static::$constraints) { - /* - * For has one relationships we need to actually query on the primary key - * of the parent model matching on the OUTGOING relationship by name. - * - * We are trying to achieve a Cypher that goes something like: - * - * MATCH (user:`User`), (user)-[:PHONE]->(phone:`Phone`) - * WHERE id(user) = 86234 - * RETURN phone; - * - * (user:`User`) represents a matching statement where - * 'user' is the parent Node's placeholder and '`User`' is the parentLabel. - * All node placeholders must be lowercased letters and will be used - * throught the query to represent the actual Node. - * - * Resulting from: - * class User extends NeoEloquent { - * - * public function phone() - * { - * return $this->hasOne('Phone', 'PHONE'); - * } - * } - */ - - // Get the parent node's placeholder. - $parentNode = $this->getParentNode(); - // Tell the query that we only need the related model returned. - $this->query->select($this->relation); - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()-[]->() Cypher clause. - $this->query->matchIn($this->parent, $this->related, $this->relation, $this->type, $this->localKey, $this->parent->{$this->localKey}); - // Add WHERE clause over the parent node's matching key = value. - $this->query->where($this->localKey, '=', $this->parent->{$this->localKey}); - } - } - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - */ - public function addEagerConstraints(array $models) - { - /* - * We'll grab the primary key name of the related models since it could be set to - * a non-standard name and not "id". We will then construct the constraint for - * our eagerly loading query so it returns the proper models from execution. - */ - parent::addEagerConstraints($models); - - // Grab the parent node placeholder - $parentNode = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - - // Tell the builder to select both models of the relationship - $this->query->select($this->relation, $parentNode); - - // Setup for their mutation so they don't breed weird stuff like... humans ?! - $this->query->addManyMutation($this->relation, $this->related); - $this->query->addManyMutation($parentNode, $this->parent); - - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()-[]->() Cypher clause. - $this->query->matchIn($this->parent, $this->related, $this->relation, $this->type, $this->localKey, $this->parent->{$this->localKey}); - // Add WHERE clause over the parent node's matching keys [values...]. - $this->query->whereIn($this->localKey, $this->getKeys($models)); - - parent::addEagerConstraints($models); - } - - /** - * Get the edge between the parent model and the given model or - * the related model determined by the relation function name. - * - * @param \Illuminate\Database\Eloquent\Model $model - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Edge[In|Out] - */ - public function edge(Model $model = null) - { - return $this->finder->first($this->parent, $model, $this->type, $this->edgeDirection); - } - - /** - * Get an instance of the Edge[In|Out] relationship. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @param array $attributes - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Edge[In|Out] - */ - public function getEdge(Model $model = null, $attributes = array()) - { - $model = (!is_null($model)) ? $model : $this->related; - - return new EdgeIn($this->query, $this->parent, $model, $this->type, $attributes); - } -} diff --git a/src/Eloquent/Relations/HasMany.php b/src/Eloquent/Relations/HasMany.php deleted file mode 100644 index 7c2a6015..00000000 --- a/src/Eloquent/Relations/HasMany.php +++ /dev/null @@ -1,93 +0,0 @@ -query->get(); - } - - /** - * Get an instance of the Edge relationship. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @param array $attributes - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut - */ - public function getEdge(Model $model = null, $attributes = array()) - { - $model = (!is_null($model)) ? $model : $this->parent->{$this->relation}; - - return new EdgeOut($this->query, $this->parent, $model, $this->type, $attributes); - } - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - */ - public function addEagerConstraints(array $models) - { - /* - * We'll grab the primary key name of the related models since it could be set to - * a non-standard name and not "id". We will then construct the constraint for - * our eagerly loading query so it returns the proper models from execution. - */ - // parent::addEagerConstraints($models); - - // Grab the parent node placeholder - $parentNode = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - - // Tell the builder to select both models of the relationship - $this->query->select($this->relation, $parentNode); - - // Setup for their mutation so they don't breed weird stuff like... humans ?! - $this->query->addManyMutation($this->relation, $this->related, 'many'); - $this->query->addManyMutation($parentNode, $this->parent, 'many'); - - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()-[]->() Cypher clause. - $this->query->matchOut($this->parent, $this->related, $this->relation, $this->type, $this->localKey, $this->parent->{$this->localKey}); - // Add WHERE clause over the parent node's matching keys [values...]. - $this->query->whereIn($this->localKey, $this->getKeys($models, $this->localKey)); - - } - - /** - * Match the eagerly loaded results to their parents. - * - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $relation - * - * @return array - */ - public function match(array $models, Collection $results, $relation) - { - return $this->matchMany($models, $results, $relation); - } - - /** - * Get the fully qualified parent key name. - * - * @return string - */ - public function getQualifiedParentKeyName() - { - return $this->relation; - } -} diff --git a/src/Eloquent/Relations/HasOne.php b/src/Eloquent/Relations/HasOne.php deleted file mode 100644 index acd622e6..00000000 --- a/src/Eloquent/Relations/HasOne.php +++ /dev/null @@ -1,170 +0,0 @@ -setRelation($relation, null); - } - - return $models; - } - - /** - * Set the base constraints on the relation query. - */ - public function addConstraints() - { - if (static::$constraints) { - /* - * For has one relationships we need to actually query on the primary key - * of the parent model matching on the OUTGOING relationship by name. - * - * We are trying to achieve a Cypher that goes something like: - * - * MATCH (user:`User`), (user)-[:PHONE]->(phone:`Phone`) - * WHERE id(user) = 86234 - * RETURN phone; - * - * (user:`User`) represents a matching statement where - * 'user' is the parent Node's placeholder and '`User`' is the parentLabel. - * All node placeholders must be lowercased letters and will be used - * throught the query to represent the actual Node. - * - * Resulting from: - * class User extends NeoEloquent { - * - * public function phone() - * { - * return $this->hasOne('Phone', 'PHONE'); - * } - * } - */ - - // Get the parent node's placeholder. - $parentNode = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - // Tell the query that we only need the related model returned. - $this->query->select($this->relation); - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()-[]->() Cypher clause. - $this->query->matchOut($this->parent, $this->related, $this->relation, $this->type, $this->localKey, $this->parent->{$this->localKey}); - // Add WHERE clause over the parent node's matching key = value. - $this->query->where($this->localKey, '=', $this->parent->{$this->localKey}); - } - } - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - */ - public function addEagerConstraints(array $models) - { - /* - * We'll grab the primary key name of the related models since it could be set to - * a non-standard name and not "id". We will then construct the constraint for - * our eagerly loading query so it returns the proper models from execution. - */ - parent::addEagerConstraints($models); - - // Grab the parent node placeholder - $parentNode = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - - // $this->query->startModel = $this->parent; - // $this->query->endModel = $this->related; - // $this->query->relationshipName = $this->relation; - - // Tell the builder to select both models of the relationship - $this->query->select($this->relation, $parentNode); - - // Setup for their mutation so they don't breed weird stuff like... humans ?! - $this->query->addMutation($this->relation, $this->related); - $this->query->addMutation($parentNode, $this->parent); - - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()-[]->() Cypher clause. - $this->query->matchOut($this->parent, $this->related, $this->relation, $this->type, $this->localKey, $this->parent->{$this->localKey}); - // Add WHERE clause over the parent node's matching keys [values...]. - $this->query->whereIn($this->localKey, $this->getKeys($models)); - - } - - /** - * Get an instance of the EdgeIn relationship. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @param array $attributes - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut - */ - public function getEdge(Model $model = null, $attributes = array()) - { - $model = (!is_null($model)) ? $model : $this->related; - - // Indicate a unique relation since this only involves one other model. - $unique = true; - - return new EdgeOut($this->query, $this->parent, $model, $this->type, $attributes, $unique); - } - - /** - * Get the edge between the parent model and the given model or - * the related model determined by the relation function name. - * - * @param \Illuminate\Database\Eloquent\Model $model - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Edge[In,Out, etc.] - */ - public function edge(Model $model = null) - { - return $this->getEdge($model)->current(); - } - - /** - * Match the eagerly loaded results to their parents. - * - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $relation - * - * @return array - */ - public function match(array $models, Collection $results, $relation) - { - return $this->matchOne($models, $results, $relation); - } - - /** - * Get the results of the relationship. - * - * @return mixed - */ - public function getResults() - { - return $this->query->first(); - } -} diff --git a/src/Eloquent/Relations/HasOneOrMany.php b/src/Eloquent/Relations/HasOneOrMany.php deleted file mode 100644 index fe892382..00000000 --- a/src/Eloquent/Relations/HasOneOrMany.php +++ /dev/null @@ -1,955 +0,0 @@ -localKey = $key; - $this->relation = $relation; - $this->type = $type; - - parent::__construct($query, $parent, $type, $key); - - $this->finder = $this->newFinder(); - } - - /** - * Initialize the relation on a set of models. - * - * @param array $models - * @param string $relation - * - * @return array - */ - public function initRelation(array $models, $relation) - { - foreach ($models as $model) { - // In the case of fetching nested relations, we will get an array - // with the first key being the model we need, and the other being - // the related model so we'll just take the first model out of the array. - if (is_array($model)) { - $model = reset($model); - } - // } else if ($model instanceof Relationship) { - // $model = $model->getEndModel(); - // } - - $model->setRelation($relation, $this->related->newCollection()); - } - - return $models; - } - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - */ - public function addEagerConstraints(array $models) - { - $this->query->startModel = $this->parent; - $this->query->endModel = $this->related; - $this->query->relationshipName = $this->relation; - } - - /** - * Get all of the primary keys for an array of models. - * - * @param array $models - * @param string $key - * - * @return array - */ - protected function getKeys(array $models, $key = null) - { - return array_unique(array_values(array_map(function ($value) use ($key, $models) { - if (is_array($value)) { - // $models is a collection of associative arrays with the keys being a model and its relation, - // our job is to know which one to use since if we use the first element - // it might not be what we need, in some cases it's the lat element. - // To do that we're going to reversely detect the correct identifier (key) - // to use and it's sufficient to detect it from one of the records. - $identifier = $this->determineValueIdentifier(reset($models)); - $value = $value[$identifier]; - // $value = reset($value); - // $value = end($value); - } - // } else if($value instanceof Relationship) { - // $value = $value->getEndModel(); - // } - - return $key ? $value->getAttribute($key) : $value->getKey(); - - }, $models))); - } - - /** - * Get an instance of the Edge[In, Out, etc.] relationship. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @param array $attributes - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Edge[In,Out, etc.] - */ - abstract public function getEdge(Model $model = null, $attributes = array()); - - /** - * Get the edge between the parent model and the given model or - * the related model determined by the relation function name. - * - * @param \Illuminate\Database\Eloquent\Model $model - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Edge[In,Out, etc.] - */ - public function edge(Model $model = null) - { - return $this->finder->first($this->parent, $model, $this->type, $this->edgeDirection); - } - - /** - * Get all the edges of the given type and direction. - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Edge[In|Out] - */ - public function edges() - { - return $this->finder->get($this->parent, $this->related, $this->type, $this->edgeDirection); - } - - /** - * Match the eagerly loaded results to their single parents. - * - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $relation - * - * @return array - */ - public function matchOne(array $models, Collection $results, $relation) - { - return $this->matchOneOrMany($models, $results, $relation, 'one'); - } - - /** - * Match the eagerly loaded results to their many parents. - * - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $relation - * - * @return array - */ - public function matchMany(array $models, Collection $results, $relation) - { - return $this->matchOneOrMany($models, $results, $relation, 'many'); - } - - /** - * Match the eagerly loaded results to their parents. - * - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $relation - * - * @return array - */ - public function matchOneOrMany(array $models, Collection $results, $relation, $type) - { - // $map = []; - - // foreach ($models as $model) { - // if (is_array($model)) { - // unset($model[$relation]); - // $model = array_values($model)[0]; - // } - - // $index = 'i-'.$model->getKey(); - // $map[$index] = $model; - // } - - // // We will need the parent node placeholder so that we use it to extract related results. - // $startNodeIdentifier = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - - // foreach ($results as $result) { - // if (is_array($result)) { - // $model = $result[$startNodeIdentifier]; - // } - - // $index = 'i-'.$model->getKey(); - // $model = $map[$index]; - - // switch ($type) { - // case 'one': - // default: - // $model->setRelation($relation, $result[$relation]); - // break; - // case 'many': - // $collection = $model->$relation; - // $collection->push($result[$relation]); - // $model->setRelation($relation, $collection); - // break; - // } - // } - - // foreach ($results as $result) { - // $startModel = $result->getStartModel(); - // $endModel = $result->getEndModel(); - - // $index = 'i-'.$startModel->getKey(); - // $model = $map[$index]; - - // switch ($type) { - // case 'one': - // default: - // $model->setRelation($relation, $endModel); - // break; - // case 'many': - // $collection = $model->$relation; - // $collection->push($endModel); - // $model->setRelation($relation, $collection); - // break; - // } - // } - - // return array_values($map); - - /// ---- OLD IMPLEMENTATION ------ // - - - // We will need the parent node placeholder so that we use it to extract related results. - $parent = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - - /* - * Looping into all the parents to match back onto their children using - * the primary key to map them onto the correct instances, every single - * result will be having both instances at each Collection item, held by their - * node placeholder. - */ - foreach ($models as $model) { - $matched = $results->filter(function ($result) use ($parent, $model, $models) { - if ($result[$parent] instanceof Model) { - // In the case of fetching nested relations, we will get an array - // with the first key being the model we need, and the other being - // the related model so we'll just take the first model out of the array. - if (is_array($model)) { - $identifier = $this->determineValueIdentifier($model); - $model = $model[$identifier]; - } - - return $model->getKey() == $result[$parent]->getKey(); - } - }); - - // Now that we have the matched parents we know where to add the relations. - // Sometimes we have more than a match so we gotta catch them all! - foreach ($matched as $match) { - // In the case of fetching nested relations, we will get an array - // with the first key being the model we need, and the other being - // the related model so we'll just take the first model out of the array. - if (is_array($model)) { - $identifier = $this->determineValueIdentifier($model); - $model = $model[$identifier]; - } - - if ($type == 'many') { - $collection = $this->related->newCollection(); - - if ($model->hasRelation($relation)) { - $collection = $model->getRelation($relation); - } - - $collection->push($match[$relation]); - $model->setRelation($relation, $collection); - } else { - $model->setRelation($relation, $match[$relation]); - } - } - } - - return $models; - } - - /** - * Get the value of a relationship by one or many type. - * - * @param array $dictionary - * @param string $key - * @param string $type - * - * @return mixed - */ - protected function getRelationValue(array $dictionary, $key, $type) - { - $value = $dictionary[$key]; - - return $type == 'one' ? reset($value) : $this->related->newCollection($value); - } - - /** - * Build model dictionary keyed by the relation's foreign key. - * - * @param \Illuminate\Database\Eloquent\Collection $results - * - * @return array - */ - protected function buildDictionary(Collection $results) - { - $dictionary = []; - - $foreign = $this->getPlainForeignKey(); - - // First we will create a dictionary of models keyed by the foreign key of the - // relationship as this will allow us to quickly access all of the related - // models without having to do nested looping which will be quite slow. - foreach ($results as $result) { - $dictionary[$result->{$foreign}][] = $result; - } - - return $dictionary; - } - - /** - * Attach a model instance to the parent model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @param array $properties The relationship properites - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Edge[In, Out, etc.] - */ - public function save(Model $model, array $properties = array()) - { - $model->save() ? $model : false; - // Create a new edge relationship for both models - $edge = $this->getEdge($model, $properties); - // Save the edge - $edge->save(); - - return $edge; - } - - /** - * Attach an array of models to the parent instance. - * - * @param array $models - * @param arra $properties The relationship properties - * - * @return array - */ - public function saveMany($models, array $properties = array()) - { - // We will collect the edges returned by save() in an Eloquent Database Collection - // and return them when done. - $edges = new Collection(); - - foreach ($models as $model) { - $edges->push($this->save($model, $properties)); - } - - return $edges; - } - - /** - * Create a new instance of the related model. - * - * @param array $attributes - * @param array $properties The relationship properites - * - * @return \Vinelab\NeoEloquent\Eloquent\Model - */ - public function create(array $attributes = [], array $properties = array()) - { - // Here we will set the raw attributes to avoid hitting the "fill" method so - // that we do not have to worry about a mass accessor rules blocking sets - // on the models. Otherwise, some of these attributes will not get set. - $instance = $this->related->newInstance($attributes); - - return $this->save($instance, $properties); - } - - /** - * Create an array of new instances of the related model. - * - * @param array $records - * @param array $properties The relationship properites - * - * @return array - */ - public function createMany(array $records, array $properties = array()) - { - $instances = new Collection(); - - foreach ($records as $record) { - $instances->push($this->create($record, $properties)); - } - - return $instances; - } - - /** - * Set the base constraints on the relation query. - */ - public function addConstraints() - { - if (static::$constraints) { - /* - * For has one relationships we need to actually query on the primary key - * of the parent model matching on the OUTGOING relationship by name. - * - * We are trying to achieve a Cypher that goes something like: - * - * MATCH (user:`User`), (user)-[:PHONE]->(phone:`Phone`) - * WHERE id(user) = 86234 - * RETURN phone; - * - * (user:`User`) represents a matching statement where - * 'user' is the parent Node's placeholder and '`User`' is the parentLabel. - * All node placeholders must be lowercased letters and will be used - * throught the query to represent the actual Node. - * - * Resulting from: - * class User extends NeoEloquent { - * - * public function phone() - * { - * return $this->hasOne('Phone', 'PHONE'); - * } - * } - */ - - // Get the parent node's placeholder. - $parentNode = $this->getParentNode(); - // Tell the query that we only need the related model returned. - $this->query->select($this->relation); - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($this->relation); - // Build the MATCH ()-[]->() Cypher clause. - $this->query->matchOut($this->parent, $this->related, $this->relation, $this->type, $this->localKey, $this->parent->{$this->localKey}); - // Add WHERE clause over the parent node's matching key = value. - $this->query->where($parentNode.'.'.$this->localKey, '=', $this->parent->{$this->localKey}); - } - } - - /** - * Attach a model to the parent. - * - * @param mixed $id - * @param array $attributes - * @param bool $touch - */ - public function attach($id, array $attributes = array(), $touch = true) - { - $models = $id; - - if ($id instanceof Model) { - $models = [$id]; - } elseif ($id instanceof Collection) { - $models = $id->all(); - } elseif (!$this->isArrayOfModels($id)) { - $models = $this->modelsFromIds($id); - // In case someone is messing with us and passed a bunch of ids (or single id) - // that do not exist we slap them in the face with a ModelNotFoundException. - // There must be at least a record found as for the records that do not match - // they will be ignored and forever forgotten, poor thing. - if (count($models) < 1) { - throw (new ModelNotFoundException())->setModel(get_class($this->related)); - } - - $models = $models->all(); - } - - $saved = $this->saveMany($models, $attributes); - - if ($touch) { - $this->touchIfTouching(); - } - - return (!is_array($id)) ? $saved->first() : $saved; - } - - /** - * Detach models from the relationship. - * - * @param int|array $ids - * @param bool $touch - * - * @return int - */ - public function detach($id = array(), $touch = true) - { - if (!$id instanceof Model and !$id instanceof Collection) { - $id = $this->modelsFromIds($id); - } elseif (!is_array($id)) { - $id = [$id]; - } - - /* - * @todo enhance this by creating a WHERE IN query - */ - // Prepare for a batch operation to take place so that we don't - // overwhelm the database with many delete hits. - $results = []; - foreach ($id as $model) { - $edge = $this->edge($model); - $results[] = $edge->delete(); - } - - if ($touch) { - $this->touchIfTouching(); - } - - return !in_array(false, $results); - } - - public function delete($shouldKeepEndNode = false) - { - return $this->finder->delete($shouldKeepEndNode); - } - - /** - * Sync the intermediate tables with a list of IDs or collection of models. - * - * @param $ids - * @param bool $detaching - * - * @return array - */ - public function sync($ids, $detaching = true) - { - $changes = array( - 'attached' => array(), 'detached' => array(), 'updated' => array(), - ); - - // get them as collection - if ($ids instanceof Collection) { - $ids = $ids->modelKeys(); - } elseif (!is_array($ids)) { - $ids = [$ids]; - } - - // First we need to attach the relationships that do not exist - // for this model so we'll spin throuhg the edges of this model - // for the specified type regardless of the direction and create - // those that do not exist. - - // Let's fetch the existing edges first. - $edges = $this->edges(); - // Collect the current related models IDs out of related models. - $current = array_map(function (Edge $edge) { return $edge->getRelated()->getKey(); }, $edges->toArray()); - - $records = $this->formatSyncList($ids); - - $detach = array_diff($current, array_keys($records)); - - // Next, we will take the differences of the currents and given IDs and detach - // all of the entities that exist in the "current" array but are not in the - // the array of the IDs given to the method which will complete the sync. - if ($detaching && count($detach) > 0) { - $this->detach($detach); - - $changes['detached'] = (array) array_map('intval', $detach); - } - - // Now we are finally ready to attach the new records. Note that we'll disable - // touching until after the entire operation is complete so we don't fire a - // ton of touch operations until we are totally done syncing the records. - $changes['attached'] = $records; - $changes['updated'] = $current; - - // Now we are finally ready to attach the new records. Note that we'll disable - // touching until after the entire operation is complete so we don't fire a - // ton of touch operations until we are totally done syncing the records. - $changes = array_merge( - $changes, $this->attachNew($records, $current, false) - ); - - $this->touchIfTouching(); - - return $changes; - } - - protected function attachNew(array $records, array $current, $touch = true) - { - $changes = array('attached' => array(), 'updated' => array()); - - foreach ($records as $id => $attributes) { - // If the ID is not in the list of existing pivot IDs, we will insert a new pivot - // record, otherwise, we will just update this existing record on this joining - // table, so that the developers will easily update these records pain free. - if (!in_array($id, $current)) { - $this->attach($id, $attributes, $touch); - - $changes['attached'][] = (int) $id; - } elseif (count($attributes) > 0) { - $this->updateEdge($id, $attributes); - - $changes['updated'][] = (int) $id; - } - } - - return $changes; - } - - /** - * Perform an update on all the related models. - * - * @param array $attributes - * - * @return int - */ - public function update(array $attributes) - { - if ($this->related->usesTimestamps()) { - $attributes[$this->relatedUpdatedAt()] = $this->related->freshTimestampString(); - } - - return $this->query->update($attributes); - } - - /** - * Update an edge's properties. - * - * @param int $id - * @param array $properties - * - * @return bool - */ - public function updateEdge($id, array $properties) - { - $edge = $this->finder->first($this->parent, $this->related->findOrFail($id), $this->type, $this->edgeDirection); - $edge->fill($properties); - - return $edge->save(); - } - - /** - * Format the sync list so that it is keyed by ID. - * - * @param array $records - * - * @return array - */ - protected function formatSyncList(array $records) - { - $results = array(); - - foreach ($records as $id => $attributes) { - if (!is_array($attributes)) { - list($id, $attributes) = array($attributes, array()); - } - - $results[$id] = $attributes; - } - - return $results; - } - - /** - * If we're touching the parent model, touch. - */ - public function touchIfTouching() - { - if ($this->touchingParent()) { - $this->getParent()->touch(); - } - - if ($this->getParent()->touches($this->relation)) { - $this->touch(); - } - } - - /** - * Find a model by its primary key or return new instance of the related model. - * - * @param mixed $id - * @param array $columns - * - * @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model - */ - public function findOrNew($id, $columns = ['*']) - { - if (is_null($instance = $this->find($id, $columns))) { - $instance = $this->related->newInstance(); - - $instance->setAttribute($this->getPlainForeignKey(), $this->getParentKey()); - } - - return $instance; - } - - /** - * Get the first related model record matching the attributes or instantiate it. - * - * @param array $attributes - * - * @return \Illuminate\Database\Eloquent\Model - */ - public function firstOrNew(array $attributes) - { - if (is_null($instance = $this->where($attributes)->first())) { - $instance = $this->related->newInstance($attributes); - - $instance->setAttribute($this->getPlainForeignKey(), $this->getParentKey()); - } - - return $instance; - } - - /** - * Get the first related record matching the attributes or create it. - * - * @param array $attributes - * - * @return \Illuminate\Database\Eloquent\Model - */ - public function firstOrCreate(array $attributes) - { - if (is_null($instance = $this->where($attributes)->first())) { - $instance = $this->create($attributes); - } - - return $instance; - } - - /** - * Create or update a related record matching the attributes, and fill it with values. - * - * @param array $attributes - * @param array $values - * - * @return \Illuminate\Database\Eloquent\Model - */ - public function updateOrCreate(array $attributes, array $values = []) - { - $instance = $this->firstOrNew($attributes); - - $instance->fill($values); - - $instance->save(); - - return $instance; - } - - /** - * Determine if we should touch the parent on sync. - * - * @return bool - */ - protected function touchingParent() - { - return $this->getRelated()->touches($this->guessInverseRelation()); - } - - /** - * Attempt to guess the name of the inverse of the relation. - * - * @return string - */ - protected function guessInverseRelation() - { - return Str::camel(Str::plural(class_basename($this->getParent()))); - } - - /** - * Get the related models out of their Ids. - * - * @param array $ids - * - * @return \Illuminate\Database\Eloquent\Collection - */ - public function modelsFromIds($ids) - { - // We need a Model in order to save this relationship so we try - // to whereIn the given id(s) through the related model. - return $this->related->whereIn($this->related->getKeyName(), (array) $ids)->get(); - } - - /** - * Determine whether the given array of models is actually - * an array containing model instances. In case at least one - * of the elements is not a Model this will return false. - * - * @param array $models - * - * @return bool - */ - public function isArrayOfModels($models) - { - if (!is_array($models)) { - return false; - } - - $notModels = array_filter($models, function ($model) { - return !$model instanceof Model; - }); - - return empty($notModels); - } - - /** - * Get the plain foreign key. - * - * @return string - */ - public function getPlainForeignKey() - { - return $this->relation; - } - - /** - * Get the foreign key for the relationship. - * - * @return string - */ - public function getForeignKey() - { - return $this->getForeignKey; - } - - /** - * Get the key value of the parent's local key. - * - * @return mixed - */ - public function getParentKey() - { - return $this->parent->getAttribute($this->localKey); - } - - /** - * Get the fully qualified parent key name. - * - * @return string - */ - public function getQualifiedParentKeyName() - { - return $this->parent->getTable().'.'.$this->localKey; - } - /** - * Get a new Finder instance. - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Finder - */ - public function newFinder() - { - return new Finder($this->query); - } - - /** - * Get the key for comparing against the parent key in "has" query. - * - * @return string - */ - public function getHasCompareKey() - { - return $this->related->getKeyName(); - } - - /** - * Get the relation name. - * - * @return string - */ - public function getRelationName() - { - return $this->relation; - } - - /** - * Get the relationship type (label in other words), - * [:FOLLOWS] etc. - * - * @return string - */ - public function getRelationType() - { - return $this->type; - } - - /** - * Get the localKey. - * - * @return string - */ - public function getLocalKey() - { - return $this->localKey; - } - - /** - * Get the parent model's value according to $localKey. - * - * @return mixed - */ - public function getParentLocalKeyValue() - { - return $this->parent->{$this->localKey}; - } - - /** - * Get the parent model's Node placeholder. - * - * @return string - */ - public function getParentNode() - { - return $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - } - - /** - * Get the related model's Node placeholder. - * - * @return string - */ - public function getRelatedNode() - { - return $this->query->getQuery()->modelAsNode($this->related->nodeLabel()); - } - - /** - * Get the edge direction for this relationship. - * - * @return string - */ - public function getEdgeDirection() - { - return $this->edgeDirection; - } -} diff --git a/src/Eloquent/Relations/HyperMorph.php b/src/Eloquent/Relations/HyperMorph.php deleted file mode 100644 index b3d2754e..00000000 --- a/src/Eloquent/Relations/HyperMorph.php +++ /dev/null @@ -1,139 +0,0 @@ -morph = $morph; - $this->morphType = $morphType; - - parent::__construct($query, $parent, $type, $key, $relation); - } - - /** - * Set the base constraints on the relation query. - */ - public function addConstraints() - { - if (static::$constraints) { - /* - * For has one relationships we need to actually query on the primary key - * of the parent model matching on the OUTGOING relationship by name. - * - * We are trying to achieve a Cypher that goes something like: - * - * MATCH (user:`User`), (user)-[:PHONE]->(phone:`Phone`) - * WHERE id(user) = 86234 - * RETURN phone; - * - * (user:`User`) represents a matching statement where - * 'user' is the parent Node's placeholder and '`User`' is the parentLabel. - * All node placeholders must be lowercased letters and will be used - * throught the query to represent the actual Node. - * - * Resulting from: - * class User extends NeoEloquent { - * - * public function phone() - * { - * return $this->hasOne('Phone', 'PHONE'); - * } - * } - */ - - // Get the parent node's placeholder. - $parentNode = $this->getParentNode(); - // Tell the query that we only need the related model returned. - $this->query->select($this->relation); - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()-[]->() Cypher clause. - $this->query->matchOut($this->parent, $this->related, $this->relation, $this->type, $this->localKey, $this->parent->{$this->localKey}); - // Add WHERE clause over the parent node's matching key = value. - $this->query->where($this->localKey, '=', $this->parent->{$this->localKey}); - } - } - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - */ - public function addEagerConstraints(array $models) - { - /* - * We'll grab the primary key name of the related models since it could be set to - * a non-standard name and not "id". We will then construct the constraint for - * our eagerly loading query so it returns the proper models from execution. - */ - - // Grab the parent node placeholder - $parentNode = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - - // Tell the builder to select both models of the relationship - $this->query->select($this->relation, $parentNode); - - // Setup for their mutation so they don't breed weird stuff like... humans ?! - $this->query->addManyMutation($this->relation, $this->related); - $this->query->addManyMutation($parentNode, $this->parent); - - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()-[]->() Cypher clause. - $this->query->matchOut($this->parent, $this->related, $this->relation, $this->type, $this->localKey, $this->parent->{$this->localKey}); - // Add WHERE clause over the parent node's matching keys [values...]. - $this->query->whereIn($this->localKey, $this->getKeys($models)); - } - - public function edge(Model $model = null) - { - return $this->finder->hyperFirst($this->parent, $model, $this->morph, $this->type, $this->morphType); - } - - public function getEdge(Model $model = null, $properties = array()) - { - $model = (!is_null($model)) ? $model : $this->related; - - return new HyperEdge($this->query, $this->parent, $this->type, $model, $this->morphType, $this->morph, $properties); - } -} diff --git a/src/Eloquent/Relations/MorphMany.php b/src/Eloquent/Relations/MorphMany.php deleted file mode 100644 index 07693103..00000000 --- a/src/Eloquent/Relations/MorphMany.php +++ /dev/null @@ -1,99 +0,0 @@ -(phone:`Phone`) - * WHERE id(user) = 86234 - * RETURN phone; - * - * (user:`User`) represents a matching statement where - * 'user' is the parent Node's placeholder and '`User`' is the parentLabel. - * All node placeholders must be lowercased letters and will be used - * throught the query to represent the actual Node. - * - * Resulting from: - * class User extends NeoEloquent { - * - * public function phone() - * { - * return $this->hasOne('Phone', 'PHONE'); - * } - * } - */ - - // Get the parent node's placeholder. - $parentNode = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - // Tell the query that we only need the related model returned. - $this->query->select($this->relation); - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()-[]->() Cypher clause. - $this->query->matchIn($this->parent, $this->related, $this->relation, $this->type, $this->localKey, $this->parent->{$this->localKey}); - // Add WHERE clause over the parent node's matching key = value. - $this->query->where($this->localKey, '=', $this->parent->{$this->localKey}); - } - } - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - */ - public function addEagerConstraints(array $models) - { - /* - * We'll grab the primary key name of the related models since it could be set to - * a non-standard name and not "id". We will then construct the constraint for - * our eagerly loading query so it returns the proper models from execution. - */ - - // Grab the parent node placeholder - $parentNode = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - - // Tell the builder to select both models of the relationship - $this->query->select($this->relation, $parentNode); - - // Setup for their mutation so they don't breed weird stuff like... humans ?! - $this->query->addManyMutation($this->relation, $this->related, 'many'); - $this->query->addManyMutation($parentNode, $this->parent, 'many'); - - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()-[]->() Cypher clause. - $this->query->matchIn($this->parent, $this->related, $this->relation, $this->type, $this->localKey, $this->parent->{$this->localKey}); - // Add WHERE clause over the parent node's matching keys [values...]. - $this->query->whereIn($this->localKey, $this->getKeys($models)); - } - - /** - * Get an instance of the Edge[In|Out] relationship. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @param array $attributes - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Edge[In|Out] - */ - public function getEdge(Model $model = null, $attributes = array()) - { - $model = (!is_null($model)) ? $model : $this->related; - - return new EdgeOut($this->query, $this->parent, $model, $this->type, $attributes); - } -} diff --git a/src/Eloquent/Relations/MorphTo.php b/src/Eloquent/Relations/MorphTo.php deleted file mode 100644 index 021d086d..00000000 --- a/src/Eloquent/Relations/MorphTo.php +++ /dev/null @@ -1,124 +0,0 @@ -morphType = $type; - - parent::__construct($query, $parent, $relationType, $otherKey, $relation); - } - - /** - * Set the base constraints on the relation query. - */ - public function addConstraints() - { - if (static::$constraints) { - // Get the parent node's placeholder. - $parentNode = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - // Tell the query that we need the morph model and the relationship represented by CypherGrammar - // statically with 'r'. - $this->query->select($this->relation, 'r'); - // Add morph mutation that will tell the parser about the property name on the Relationship that is holding - // the class name of our morph model so that they can instantiate the correct one, and pass the relation - // name as an indicator of the Node that has our morph attributes in the query. - $this->query->addMorphMutation($this->relation); - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()<-[]-() Cypher clause. - $this->query->matchMorphOut($this->parent, $this->relation, $this->relationType, $this->parent->{$this->relationType}); - // Add WHERE clause over the parent node's matching key = value. - $this->query->where($this->relationType, '=', $this->parent->{$this->relationType}); - } - } - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - */ - public function addEagerConstraints(array $models) - { - // Get the parent node's placeholder. - $parentNode = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - // Tell the query that we need the morph model and the relationship represented by CypherGrammar - // statically with 'r'. - $this->query->select('r', $parentNode, $this->relation); - // Add morph mutation that will tell the parser about the property name on the Relationship that is holding - // the class name of our morph model so that they can instantiate the correct one, and pass the relation - // name as an indicator of the Node that has our morph attributes in the query. - $this->query->addMutation($parentNode, $this->parent); - $this->query->addEagerMorphMutation($this->relation); - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()<-[]-() Cypher clause. - $this->query->matchMorphOut($this->parent, $this->relation, $this->relationType, $this->parent->{$this->relationType}); - // Add WHERE clause over the parent node's matching keys [values...]. - $this->query->whereIn($this->relationType, $this->getKeys($models)); - } - - /** - * Match the eagerly loaded results to their parents. - * - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $relation - * - * @return array - */ - public function match(array $models, Collection $results, $relation) - { - // This relationship deals with a One-To-One morph type so we'll just extract - // the first model out of the results and return it. - $matched = parent::match($models, $results, $relation); - - return array_map(function ($match) use ($relation) { - if (isset($match[$relation]) && isset($match[$relation][0])) { - $match->setRelation($relation, $match[$relation][0]); - } - - return $match; - - }, $matched); - } - - /** - * Get an instance of the EdgeIn relationship. - * - * @param \Vinelab\NeoEloquent\Eloquent\Model $model - * @param array $attributes - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut - */ - public function getEdge(Model $model = null, $attributes = array()) - { - $model = (!is_null($model)) ? $model : $this->parent->{$this->relation}; - - // Indicate a unique relationship since this involves one other model. - $unique = true; - - return new EdgeOut($this->query, $this->parent, $model, $this->relationType, $attributes, $unique); - } -} diff --git a/src/Eloquent/Relations/MorphedByOne.php b/src/Eloquent/Relations/MorphedByOne.php deleted file mode 100644 index 2bfc49db..00000000 --- a/src/Eloquent/Relations/MorphedByOne.php +++ /dev/null @@ -1,103 +0,0 @@ -belongsTo('User', 'PHONE'); - * } - * } - */ - - // Get the parent node's placeholder. - $parentNode = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - // Tell the query that we only need the related model returned. - $this->query->select($this->relation); - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()<-[]-() Cypher clause. - $this->query->matchOut($this->parent, $this->related, $this->relation, $this->relationType, $this->otherKey, $this->parent->{$this->otherKey}); - // Add WHERE clause over the parent node's matching key = value. - $this->query->where($this->otherKey, '=', $this->parent->{$this->otherKey}); - } - } - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - */ - public function addEagerConstraints(array $models) - { - /* - * We'll grab the primary key name of the related models since it could be set to - * a non-standard name and not "id". We will then construct the constraint for - * our eagerly loading query so it returns the proper models from execution. - */ - - // Grab the parent node placeholder - $parentNode = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - - // Tell the builder to select both models of the relationship - $this->query->select($this->relation, $parentNode); - - // Setup for their mutation so they don't breed weird stuff like... humans ?! - $this->query->addMutation($this->relation, $this->related); - $this->query->addMutation($parentNode, $this->parent); - - // Set the parent node's placeholder as the RETURN key. - $this->query->getQuery()->from = array($parentNode); - // Build the MATCH ()<-[]-() Cypher clause. - $this->query->matchOut($this->parent, $this->related, $this->relation, $this->relationType, $this->otherKey, $this->parent->{$this->otherKey}); - // Add WHERE clause over the parent node's matching keys [values...]. - $this->query->whereIn($this->otherKey, $this->getEagerModelKeys($models)); - } - - /** - * Get an instance of the EdgeIn relationship. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @param array $attributes - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut - */ - public function getEdge(Model $model = null, $attributes = array()) - { - $model = (!is_null($model)) ? $model : $this->parent->{$this->relation}; - - // Indicate a unique relation since this only involves one other model. - $unique = true; - - return new EdgeOut($this->query, $this->parent, $model, $this->relationType, $attributes, $unique); - } -} diff --git a/src/Eloquent/Relations/OneRelation.php b/src/Eloquent/Relations/OneRelation.php deleted file mode 100644 index 583d2f17..00000000 --- a/src/Eloquent/Relations/OneRelation.php +++ /dev/null @@ -1,361 +0,0 @@ -otherKey = $otherKey; - $this->relation = $relation; - $this->relationType = $relationType; - - parent::__construct($query, $parent); - } - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - */ - public function addEagerConstraints(array $models) - { - $this->query->startModel = $this->parent; - $this->query->endModel = $this->related; - $this->query->relationshipName = $this->relation; - } - - /** - * Get the results of the relationship. - * - * @return mixed - */ - public function getResults() - { - return $this->query->first(); - } - - /** - * Initialize the relation on a set of models. - * - * @param array $models - * @param string $relation - * - * @return array - */ - public function initRelation(array $models, $relation) - { - foreach ($models as $model) { - // In the case of fetching nested relations, we will get an array - // with the first key being the model we need, and the other being - // the related model so we'll just take the first model out of the array. - if (is_array($model)) { - $model = reset($model); - } - - $model->setRelation($relation, null); - } - - return $models; - } - - public function delete($shouldKeepEndNode = false) - { - return (new Finder($this->query))->delete($shouldKeepEndNode); - } - - /** - * Get an instance of the Edge[In, Out, etc.] relationship. - * - * @param \Vinelab\NeoEloquent\Eloquent\Model $model - * @param array $attributes - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Edge[In,Out, etc.] - */ - abstract public function getEdge(Model $model = null, $attributes = array()); - - /** - * Get the direction of the edge for this relationship. - * - * @return string - */ - public function getEdgeDirection() - { - return $this->edgeDirection; - } - - /** - * Associate the model instance to the given parent. - * - * @param \Illuminate\Database\Eloquent\Model $model - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Edge - */ - public function associate($model, $attributes = array()) - { - /* - * For associated models we will need to create a unique relationship - * between the parent and the related model. In Cypher we can use the - * MERGE clause to make sure that the relationship doesn't happen more than once. - * - * An example query would be like: - * - * HasOne: - * ------- - * - * MATCH (user:`User`), (phone:`Phone`) - * WHERE id(user) = 10892 AND id(phone) = 98522 - * MERGE (user)-[rel:PHONE]-(phone) - * RETURN rel; - * - * BelongsTo: - * --------- - * - * MATCH (account:`Account`), (user:`User`) - * WHERE id(account) = 10892 AND id(user) = 98522 - * MERGE (account)<-[rel:ACCOUNT]-(user) - * RETURN rel; - */ - - // Set the relation on the model - $this->parent->setRelation($this->relation, $model); - - /* - * Due to the fact that relationships in Graph are entities themselves - * we will need to treat them as such and in this case what we're looking for is - * a relationship with an INCOMING direction towards the parent node, in other words - * it is a relationship with an edge incoming towards the $parent model and we call it - * an "Edge" relationship. - */ - $relation = $this->getEdge($model, $attributes); - - $relation->save(); - - return $relation; - } - - /** - * Dissociate previously associated model from the given parent. - * - * @return \Illuminate\Database\Eloquent\Model - */ - public function dissociate() - { - $this->parent->setAttribute($this->relationType, null); - - return $this->parent->setRelation($this->relation, null); - } - - /** - * Update the parent model on the relationship. - * - * @param array $attributes - * - * @return mixed - */ - public function update(array $attributes) - { - $instance = $this->getResults(); - - return $instance->fill($attributes)->save(); - } - - /** - * Get the fully qualified associated key of the relationship. - * - * @return string - */ - public function getQualifiedOtherKeyName() - { - return $this->otherKey; - } - - /** - * Get the associated key of the relationship. - * - * @return string - */ - public function getOtherKey() - { - return $this->otherKey; - } - - /** - * Get the edge between the parent model and the given model or - * the related model determined by the relation function name. - * - * @param \Vinelab\NeoEloquent\Eloquent\Model $model - * - * @return \Vinelab\NeoEloquent\Eloquent\Edges\Edge[In,Out, etc.] - */ - public function edge(Model $model = null) - { - return $this->getEdge($model)->current(); - } - - /** - * Gather the keys from an array of related models. - * - * @param array $models - * - * @return array - */ - protected function getEagerModelKeys(array $models) - { - $keys = array(); - - /* - * First we need to gather all of the keys from the parent models so we know what - * to query for via the eager loading query. We will add them to an array then - * execute a "where in" statement to gather up all of those related records. - */ - foreach ($models as $model) { - // In the case of fetching nested relations, we will get an array - // with the first key being the model we need, and the other being - // the related model so we'll just take the first model out of the array. - if (is_array($model)) { - $model = reset($model); - } - - if (!is_null($value = $model->{$this->otherKey})) { - $keys[] = $value; - } - } - - /* - * If there are no keys that were not null we will just return an empty array in - * it so the query doesn't fail, but will not return any results, which should - * be what this developer is expecting in a case where this happens to them. - */ - if (count($keys) == 0) { - return array(); - } - - return array_values(array_unique($keys)); - } - - /** - * Match the eagerly loaded results to their parents. - * - * @param array $models - * @param \Vinelab\NeoEloquent\Eloquent\Collection $results - * @param string $relation - * - * @return array - */ - public function match(array $models, Collection $results, $relation) - { - // We will need the parent node placeholder so that we use it to extract related results. - $parent = $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - - /* - * Looping into all the parents to match back onto their children using - * the primary key to map them onto the correct instances, every single - * result will be having both instances at each Collection item, held by their - * node placeholder. - */ - foreach ($models as $model) { - $matched = $results->filter(function ($result) use ($parent, $model) { - if ($result[$parent] instanceof Model) { - // In the case of fetching nested relations, we will get an array - // with the first key being the model we need, and the other being - // the related model so we'll just take the first model out of the array. - if (is_array($model)) { - $identifier = $this->determineValueIdentifier($model); - $model = $model[$identifier]; - } - - return $model->getKey() == $result[$parent]->getKey(); - } - }); - - // Now that we have the matched parents we know where to add the relations. - // Sometimes we have more than a match so we gotta catch them all! - foreach ($matched as $match) { - // In the case of fetching nested relations, we will get an array - // with the first key being the model we need, and the other being - // the related model so we'll just take the first model out of the array. - if (is_array($model)) { - $identifier = $this->determineValueIdentifier($model); - $model = $model[$identifier]; - } - - $model->setRelation($relation, $match[$relation]); - } - } - - return $models; - } - - public function getRelationName() - { - return $this->relation; - } - - public function getRelationType() - { - return $this->relationType; - } - - public function getParentNode() - { - return $this->query->getQuery()->modelAsNode($this->parent->nodeLabel()); - } - - public function getRelatedNode() - { - return $this->query->getQuery()->modelAsNode($this->related->nodeLabel()); - } - - public function getLocalKey() - { - return $this->otherKey; - } - - public function getParentLocalKeyValue() - { - return $this->parent->{$this->otherKey}; - } -} diff --git a/src/Eloquent/Relations/Relation.php b/src/Eloquent/Relations/Relation.php deleted file mode 100644 index 5fc0cdaf..00000000 --- a/src/Eloquent/Relations/Relation.php +++ /dev/null @@ -1,339 +0,0 @@ -query = $query; - $this->parent = $parent; - $this->related = $query->getModel(); - - $this->addConstraints(); - } - - /** - * Set the base constraints on the relation query. - */ - abstract public function addConstraints(); - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - */ - abstract public function addEagerConstraints(array $models); - - /** - * Initialize the relation on a set of models. - * - * @param array $models - * @param string $relation - * - * @return array - */ - abstract public function initRelation(array $models, $relation); - - /** - * Match the eagerly loaded results to their parents. - * - * @param array $models - * @param \Vinelab\NeoEloquent\Eloquent\Collection $results - * @param string $relation - * - * @return array - */ - abstract public function match(array $models, Collection $results, $relation); - - /** - * Get the results of the relationship. - * - * @return mixed - */ - abstract public function getResults(); - - /** - * Get the relationship for eager loading. - * - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getEager() - { - return $this->get(); - } - - /** - * Touch all of the related models for the relationship. - */ - public function touch() - { - $column = $this->getRelated()->getUpdatedAtColumn(); - - $this->rawUpdate([$column => $this->getRelated()->freshTimestampString()]); - } - - /** - * Run a raw update against the base query. - * - * @param array $attributes - * - * @return int - */ - public function rawUpdate(array $attributes = []) - { - return $this->query->update($attributes); - } - - /** - * Add the constraints for a relationship count query. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param \Illuminate\Database\Eloquent\Builder $parent - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function getRelationCountQuery(Builder $query, Builder $parent) - { - $query->select(new Expression('count(*)')); - - $key = $this->wrap($this->getQualifiedParentKeyName()); - - return $query->where($this->getHasCompareKey(), '=', new Expression($key)); - } - - /** - * Run a callback with constraints disabled on the relation. - * - * @param \Closure $callback - * - * @return mixed - */ - public static function noConstraints(Closure $callback) - { - $previous = static::$constraints; - - static::$constraints = false; - - // When resetting the relation where clause, we want to shift the first element - // off of the bindings, leaving only the constraints that the developers put - // as "extra" on the relationships, and not original relation constraints. - $results = call_user_func($callback); - - static::$constraints = $previous; - - return $results; - } - - /** - * When matching eager loaded data, we need to determine - * which identifier should be used to set the related models to. - * This is done by iterating the given models and checking for - * the matching class between the result and this relation's - * parent model. When there's a match, the identifier at which - * the match occurred is returned. - * - * @param array $models - * - * @return string - */ - protected function determineValueIdentifier(array $models) - { - foreach ($models as $resultIdentifier => $model) { - if (get_class($this->parent) === get_class($model)) { - return $resultIdentifier; - } - } - } - - /** - * Get all of the primary keys for an array of models. - * - * @param array $models - * @param string $key - * - * @return array - */ - protected function getKeys(array $models, $key = null) - { - return array_unique(array_values(array_map(function ($value) use ($key) { - return $key ? $value->getAttribute($key) : $value->getKey(); - - }, $models))); - } - - /** - * Get the underlying query for the relation. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function getQuery() - { - return $this->query; - } - - /** - * Get the base query builder driving the Eloquent builder. - * - * @return \Illuminate\Database\Query\Builder - */ - public function getBaseQuery() - { - return $this->query->getQuery(); - } - - /** - * Get the parent model of the relation. - * - * @return \Illuminate\Database\Eloquent\Model - */ - public function getParent() - { - return $this->parent; - } - - /** - * Get the fully qualified parent key name. - * - * @return string - */ - public function getQualifiedParentKeyName() - { - return $this->parent->getQualifiedKeyName(); - } - - /** - * Get the related model of the relation. - * - * @return \Illuminate\Database\Eloquent\Model - */ - public function getRelated() - { - return $this->related; - } - - /** - * Get the name of the "created at" column. - * - * @return string - */ - public function createdAt() - { - return $this->parent->getCreatedAtColumn(); - } - - /** - * Get the name of the "updated at" column. - * - * @return string - */ - public function updatedAt() - { - return $this->parent->getUpdatedAtColumn(); - } - - /** - * Get the name of the related model's "updated at" column. - * - * @return string - */ - public function relatedUpdatedAt() - { - return $this->related->getUpdatedAtColumn(); - } - - /** - * Wrap the given value with the parent query's grammar. - * - * @param string $value - * - * @return string - */ - public function wrap($value) - { - return $this->parent->newQueryWithoutScopes()->getQuery()->getGrammar()->wrap($value); - } - - /** - * Set the morph map for polymorphic relations. - * - * @param array|null $map - * @param bool $merge - * - * @return array - */ - public static function morphMap(array $map = null, $merge = true) - { - if (is_array($map)) { - static::$morphMap = $merge ? array_merge(static::$morphMap, $map) : $map; - } - - return static::$morphMap; - } - - /** - * Handle dynamic method calls to the relationship. - * - * @param string $method - * @param array $parameters - * - * @return mixed - */ - public function __call($method, $parameters) - { - $result = call_user_func_array([$this->query, $method], $parameters); - - if ($result === $this->query) { - return $this; - } - - return $result; - } -} diff --git a/src/Eloquent/Relations/RelationInterface.php b/src/Eloquent/Relations/RelationInterface.php deleted file mode 100644 index e812253b..00000000 --- a/src/Eloquent/Relations/RelationInterface.php +++ /dev/null @@ -1,56 +0,0 @@ -startNode = $startNode; - $this->endNode = $endNode; - $this->startModel = $startModel; - $this->endModel = $endModel; - } - - public function getStartNode() - { - return $this->startNode; - } - - public function getEndNode() - { - return $this->endNode; - } - - public function getStartModel() - { - return $this->startModel; - } - - public function getEndModel() - { - return $this->endModel; - } -} diff --git a/src/Eloquent/ScopeInterface.php b/src/Eloquent/ScopeInterface.php deleted file mode 100644 index f1514a28..00000000 --- a/src/Eloquent/ScopeInterface.php +++ /dev/null @@ -1,25 +0,0 @@ -forceDeleting = true; - - $this->delete(); - - $this->forceDeleting = false; - } - - /** - * Perform the actual delete query on this model instance. - */ - protected function performDeleteOnModel() - { - if ($this->forceDeleting) { - return $this->withTrashed()->where($this->getKeyName(), $this->getKey())->forceDelete(); - } - - return $this->runSoftDelete(); - } - - /** - * Perform the actual delete query on this model instance. - */ - protected function runSoftDelete() - { - $query = $this->newQuery()->where($this->getKeyName(), $this->getKey()); - - $this->{$this->getDeletedAtColumn()} = $time = $this->freshTimestamp(); - - $query->update([$this->getDeletedAtColumn() => $this->fromDateTime($time)]); - } - - /** - * Restore a soft-deleted model instance. - * - * @return bool|null - */ - public function restore() - { - // If the restoring event does not return false, we will proceed with this - // restore operation. Otherwise, we bail out so the developer will stop - // the restore totally. We will clear the deleted timestamp and save. - if ($this->fireModelEvent('restoring') === false) { - return false; - } - - $this->{$this->getDeletedAtColumn()} = null; - - // Once we have saved the model, we will fire the "restored" event so this - // developer will do anything they need to after a restore operation is - // totally finished. Then we will return the result of the save call. - $this->exists = true; - - $result = $this->save(); - - $this->fireModelEvent('restored', false); - - return $result; - } - - /** - * Determine if the model instance has been soft-deleted. - * - * @return bool - */ - public function trashed() - { - return !is_null($this->{$this->getDeletedAtColumn()}); - } - - /** - * Get a new query builder that includes soft deletes. - * - * @return \Illuminate\Database\Eloquent\Builder|static - */ - public static function withTrashed() - { - return (new static())->newQueryWithoutScope(new SoftDeletingScope()); - } - - /** - * Get a new query builder that only includes soft deletes. - * - * @return \Illuminate\Database\Eloquent\Builder|static - */ - public static function onlyTrashed() - { - $instance = new static(); - - $column = $instance->getQualifiedDeletedAtColumn(); - - return $instance->newQueryWithoutScope(new SoftDeletingScope())->whereNotNull($column); - } - - /** - * Register a restoring model event with the dispatcher. - * - * @param \Closure|string $callback - */ - public static function restoring($callback) - { - static::registerModelEvent('restoring', $callback); - } - - /** - * Register a restored model event with the dispatcher. - * - * @param \Closure|string $callback - */ - public static function restored($callback) - { - static::registerModelEvent('restored', $callback); - } - - /** - * Get the name of the "deleted at" column. - * - * @return string - */ - public function getDeletedAtColumn() - { - return defined('static::DELETED_AT') ? static::DELETED_AT : 'deleted_at'; - } - - /** - * Get the fully qualified "deleted at" column. - * - * @return string - */ - public function getQualifiedDeletedAtColumn() - { - return $this->getDeletedAtColumn(); - } -} diff --git a/src/Eloquent/SoftDeletingScope.php b/src/Eloquent/SoftDeletingScope.php deleted file mode 100644 index 1156c7f5..00000000 --- a/src/Eloquent/SoftDeletingScope.php +++ /dev/null @@ -1,150 +0,0 @@ -whereNull($model->getQualifiedDeletedAtColumn()); - - $this->extend($builder); - } - - /** - * Remove the scope from the given Eloquent query builder. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - * @param \Illuminate\Database\Eloquent\Model $model - */ - public function remove(Builder $builder, Model $model) - { - $column = $model->getQualifiedDeletedAtColumn(); - - $query = $builder->getQuery(); - - $query->wheres = collect($query->wheres)->reject(function ($where) use ($column) { - return $this->isSoftDeleteConstraint($where, $column); - })->values()->all(); - } - - /** - * Extend the query builder with the needed functions. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - */ - public function extend(Builder $builder) - { - foreach ($this->extensions as $extension) { - $this->{"add{$extension}"}($builder); - } - - $builder->onDelete(function (Builder $builder) { - $column = $this->getDeletedAtColumn($builder); - - return $builder->update([ - $column => $builder->getModel()->freshTimestampString(), - ]); - }); - } - - /** - * Get the "deleted at" column for the builder. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - * - * @return string - */ - protected function getDeletedAtColumn(Builder $builder) - { - if (count($builder->getQuery()->joins) > 0) { - return $builder->getModel()->getQualifiedDeletedAtColumn(); - } else { - return $builder->getModel()->getDeletedAtColumn(); - } - } - - /** - * Add the force delete extension to the builder. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - */ - protected function addForceDelete(Builder $builder) - { - $builder->macro('forceDelete', function (Builder $builder) { - return $builder->getQuery()->delete(); - }); - } - - /** - * Add the restore extension to the builder. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - */ - protected function addRestore(Builder $builder) - { - $builder->macro('restore', function (Builder $builder) { - $builder->withTrashed(); - - return $builder->update([$builder->getModel()->getDeletedAtColumn() => null]); - }); - } - - /** - * Add the with-trashed extension to the builder. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - */ - protected function addWithTrashed(Builder $builder) - { - $builder->macro('withTrashed', function (Builder $builder) { - $this->remove($builder, $builder->getModel()); - - return $builder; - }); - } - - /** - * Add the only-trashed extension to the builder. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - */ - protected function addOnlyTrashed(Builder $builder) - { - $builder->macro('onlyTrashed', function (Builder $builder) { - $model = $builder->getModel(); - - $this->remove($builder, $model); - - $builder->getQuery()->whereNotNull($model->getQualifiedDeletedAtColumn()); - - return $builder; - }); - } - - /** - * Determine if the given where clause is a soft delete constraint. - * - * @param array $where - * @param string $column - * - * @return bool - */ - protected function isSoftDeleteConstraint(array $where, $column) - { - return $where['type'] == 'Null' && $where['column'] == $column; - } -} diff --git a/src/Events/QueryExecuted.php b/src/Events/QueryExecuted.php deleted file mode 100644 index dfaf0c57..00000000 --- a/src/Events/QueryExecuted.php +++ /dev/null @@ -1,58 +0,0 @@ -cypher = $cypher; - $this->time = $time; - $this->bindings = $bindings; - $this->connection = $connection; - $this->connectionName = $connection->getName(); - } -} diff --git a/src/Exceptions/ConnectionException.php b/src/Exceptions/ConnectionException.php deleted file mode 100644 index b64c96f3..00000000 --- a/src/Exceptions/ConnectionException.php +++ /dev/null @@ -1,7 +0,0 @@ -query = $query; - $this->bindings = $bindings; - $this->exception = $exception; - } - - /** - * return the query. - * - * @return string - */ - public function getQuery() - { - return $this->query; - } - - /** - * return the bindings. - * - * @return string - */ - public function getBindings() - { - return $this->bindings; - } - - /** - * return the driver's exception. - * - * @return string - */ - public function getDriverException() - { - return $this->exception; - } -} diff --git a/src/Exceptions/InvalidCypherGrammarComponentException.php b/src/Exceptions/InvalidCypherGrammarComponentException.php deleted file mode 100644 index 8fffc95b..00000000 --- a/src/Exceptions/InvalidCypherGrammarComponentException.php +++ /dev/null @@ -1,7 +0,0 @@ -model = $model; - - $this->message = "No query results for model [{$model}]."; - - return $this; - } - - /** - * Get the affected Eloquent model. - * - * @return string - */ - public function getModel() - { - return $this->model; - } -} diff --git a/src/Exceptions/NoEdgeDirectionException.php b/src/Exceptions/NoEdgeDirectionException.php deleted file mode 100644 index b40c8365..00000000 --- a/src/Exceptions/NoEdgeDirectionException.php +++ /dev/null @@ -1,7 +0,0 @@ -formatMessage($exception); - - parent::__construct($message); - } - // In case this exception is an instance of any other exception that we should not be handling - // then we throw it as is. - elseif ($exception instanceof \Exception) { - throw $exception; - } - // We'll just add the query that was run. - else { - parent::__construct($query); - } - } - - /** - * Format the message that should be printed out for devs. - * - * @param \Neoxygen\NeoClient\Exception\Neo4jException $exception - * - * @return string - */ - protected function formatMessage(Neo4jException $exception) - { - $e = substr($exception->getMessage(), strpos($exception->getMessage(), 'Neo4j Exception with code ') + 26, strpos($exception->getMessage(), ' and message') - 26); - - $message = substr($exception->getMessage(), strpos($exception->getMessage(), 'message ') + 8); - - $exceptionName = $e ? $e.': ' : Neo4jException::class; - $message = $message ? $message : $exception->getMessage(); - - return $exceptionName.$message; - } -} diff --git a/src/Exceptions/UnknownDirectionException.php b/src/Exceptions/UnknownDirectionException.php deleted file mode 100644 index b4d8172e..00000000 --- a/src/Exceptions/UnknownDirectionException.php +++ /dev/null @@ -1,7 +0,0 @@ -connection($name)->getSchemaBuilder(); - } - - /** - * Get the registered name of the component. - * - * @return string - */ - protected static function getFacadeAccessor() - { - return static::$app['db']->connection()->getSchemaBuilder(); - } -} diff --git a/src/Grammars/CypherGrammar.php b/src/Grammars/CypherGrammar.php new file mode 100644 index 00000000..53835a66 --- /dev/null +++ b/src/Grammars/CypherGrammar.php @@ -0,0 +1,348 @@ +|WeakReference> */ + private static array $cache = []; + + /** + * The components that make up a select clause. + * + * @var string[] + */ + protected $selectComponents = [ + 'from', // MATCH for single node + 'joins', // MATCH with relationship and another node + + 'wheres', // WHERE + 'havings', // WHERE + + 'groups', // WITH and aggregating function + 'aggregate', // WITH and aggregating function + + 'columns', // RETURN + 'orders', // ORDER BY + 'limit', // LIMIT + 'offset', // SKIP + ]; + + public function compileSelect(Builder $query): string + { + return $this->cache($query, static fn () => IlluminateToQueryStructurePipeline::create() + ->withWheres() + ->withReturn() + ->pipe($query) + ->toCypher() + ); + } + + public function compileWheres(Builder $query): string + { + return $this->cache($query, static fn () => IlluminateToQueryStructurePipeline::create() + ->withWheres() + ->withReturn() + ->pipe($query) + ->toCypher(GrammarPipeline::create()->withWhereGrammar()) + ); + } + + public function prepareBindingForJsonContains($binding): string + { + throw new BadMethodCallException('Json contains is not supported in Neo4j'); + } + + public function compileRandom($seed): string + { + return 'random()'; + } + + public function compileExists(Builder $query): string + { + return $this->cache($query, static fn () => IlluminateToQueryStructurePipeline::create() + ->withWheres() + ->withReturn() + ->pipe($query) + ->toCypher(GrammarPipeline::create()->withWhereGrammar()) + ); + } + + public function compileInsert(Builder $query, array $values): string + { + $prefix = ''; + if (Tracer::isInBelongsToManyWithRelationship($query)) { + $prefix = 'UNWIND $toCreate AS toCreate '; + } + + $pipeline = IlluminateToQueryStructurePipeline::create()->withWheres(); + + if (is_int(array_key_first($values))) { + $pipeline = $pipeline->withBatchCreate($values); + } else { + $pipeline = $pipeline->withCreate($values); + } + + return $this->cache($query, static fn () => $prefix.$pipeline->pipe($query)->toCypher()); + } + + public function compileInsertOrIgnore(Builder $query, array $values): string + { + throw new BadMethodCallException('Compile Insert or Ignore not supported by Neo4j'); + } + + public function compileInsertGetId(Builder $query, $values, $sequence): string + { + return $this->cache($query, static fn () => IlluminateToQueryStructurePipeline::create() + ->withCreate($values) + ->withReturn() + ->pipe($query) + ->toCypher() + ); + } + + public function compileInsertUsing(Builder $query, array $columns, string $sql): string + { + throw new RuntimeException('This database engine does not support is compile insert using yet'); + } + + public function compileUpdate(Builder $query, array $values): string + { + return $this->cache($query, static fn () => IlluminateToQueryStructurePipeline::create() + ->withWheres() + ->withSet($values) + ->withReturn() + ->pipe($query) + ->toCypher() + ); + } + + public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update): string + { + return $this->cache($query, static fn () => IlluminateToQueryStructurePipeline::create() + ->withWheres() + ->withMerge($values, $uniqueBy, $update) + ->withReturn() + ->pipe($query) + ->toCypher() + ); + } + + public function prepareBindingsForUpdate(array $bindings, array $values): array + { + return []; + } + + public function compileDelete(Builder $query): string + { + return $this->cache($query, static fn () => IlluminateToQueryStructurePipeline::create() + ->withWheres() + ->withDelete() + ->withReturn() + ->pipe($query) + ->toCypher() + ); + } + + public function prepareBindingsForDelete(array $bindings): array + { + return []; + } + + /** + * @return array> + */ + public function compileTruncate(Builder $query): array + { + $cypher = $this->cache($query, static fn () => IlluminateToQueryStructurePipeline::create() + ->withDelete() + ->pipe($query) + ->toCypher() + ); + + return [$cypher => []]; + } + + public function supportsSavepoints(): bool + { + return false; + } + + /** + * @param string $name + */ + public function compileSavepoint($name): string + { + throw new BadMethodCallException('Savepoints are not supported by this driver.'); + } + + /** + * @param string $name + */ + public function compileSavepointRollBack($name): string + { + throw new BadMethodCallException('Savepoints are not supported by this driver.'); + } + + public function getOperators(): array + { + return [ + '=', + '==', + '===', + 'CONTAINS', + 'STARTS WITH', + 'ENDS WITH', + 'IN', + 'LIKE', + '=~', + '>', + '>=', + '<', + '<=', + '<>', + '!=', + '!==', + ]; + } + + public function getBitwiseOperators(): array + { + return []; + } + + public function compileJsonValueCast($value): string + { + throw new RuntimeException('This database engine does not support JSON value cast operations.'); + } + + public function whereFullText(Builder $query, $where) + { + throw new RuntimeException('This database engine does not support Full Text operations yet.'); + } + + public function whereExpression(Builder $query, $where) + { + throw new RuntimeException('This database engine does not support solo Where expressions yet.'); + } + + public function wrapArray(array $values) + { + throw new RuntimeException('This database engine does not support solo wrap Array expressions yet.'); + } + + public function wrap($value, $prefixAlias = false) + { + throw new RuntimeException('This database engine does not support solo wrap expressions.'); + } + + public function columnize(array $columns) + { + throw new RuntimeException('This database engine does not support wrap expressions yet.'); + } + + public function parameterize(array $values) + { + throw new RuntimeException('This database engine does not support solo parametrization yet.'); + } + + public function parameter($value) + { + throw new RuntimeException('This database engine does not support solo parametrization yet.'); + } + + public function quoteString($value) + { + throw new RuntimeException('This database engine does not support string quotation yet.'); + } + + public function escape($value, $binary = false) + { + throw new RuntimeException('This database engine does not support string escapes yet.'); + } + + public function isExpression($value) + { + throw new RuntimeException('This database engine does not support is expression yet.'); + } + + public function getValue($expression) + { + throw new RuntimeException('This database engine does not support is get value yet.'); + } + + public function getDateFormat(): string + { + return 'Y-m-d H:i:s'; + } + + public function getTablePrefix() + { + throw new RuntimeException('This database engine does not support is table prefixes yet.'); + } + + public function setTablePrefix($prefix) + { + throw new RuntimeException('This database engine does not support is table prefixes yet.'); + } + + public function wrapTable($table) + { + throw new RuntimeException('This database engine does not support is table wrapping yet.'); + } + + /** + * @param Closure():string $function + */ + private function cache(Builder $query, Closure $function): string + { + $cypher = $function(); + + self::$cache[$cypher] = WeakReference::create($query); + + return $cypher; + } + + /** + * @return array + */ + public static function getBindings(string $cypher): array + { + $reference = self::$cache[$cypher] ?? null; + if (is_array($reference)) { + return $reference; + } + + $builder = $reference?->get(); + + if ($builder instanceof Builder) { + return IlluminateToQueryStructurePipeline::getBindings($builder); + } + + return []; + } + + /** + * @param array $bindings + */ + public static function setBindings(string $cypher, array $bindings): void + { + self::$cache[$cypher] = $bindings; + } +} diff --git a/src/Helpers.php b/src/Helpers.php deleted file mode 100644 index 5f7770f8..00000000 --- a/src/Helpers.php +++ /dev/null @@ -1,18 +0,0 @@ -registerRepository(); - - // Once we have registered the migrator instance we will go ahead and register - // all of the migration related commands that are used by the "Artisan" CLI - // so that they may be easily accessed for registering with the consoles. - $this->registerMigrator(); - - $this->registerCommands(); - } - - /** - * Register the migration repository service. - * - * @return void - */ - protected function registerRepository() - { - $this->app->singleton('neoeloquent.migration.repository', function($app) - { - $model = new MigrationModel; - - $label = $app['config']['database.migrations_node']; - - if (isset($label)) { - $model->setLabel($label); - } - - return new DatabaseMigrationRepository( - $app['db'], - $app['db']->connection('neo4j')->getSchemaBuilder(), - $model - ); - }); - } - - /** - * Register the migrator service. - * - * @return void - */ - protected function registerMigrator() - { - // The migrator is responsible for actually running and rollback the migration - // files in the application. We'll pass in our database connection resolver - // so the migrator can resolve any of these connections when it needs to. - $this->app->singleton('neoeloquent.migrator', function($app) { - $repository = $app['neoeloquent.migration.repository']; - - return new Migrator($repository, $app['db'], $app['files']); - }); - } - - - /** - * Register all of the migration commands. - * - * @return void - */ - protected function registerCommands() - { - $commands = array( - 'Migrate', - 'MigrateRollback', - 'MigrateReset', - 'MigrateRefresh', - 'MigrateMake' - ); - - // We'll simply spin through the list of commands that are migration related - // and register each one of them with an application container. They will - // be resolved in the Artisan start file and registered on the console. - foreach ($commands as $command) - { - $this->{'register'.$command.'Command'}(); - } - - // Once the commands are registered in the application IoC container we will - // register them with the Artisan start event so that these are available - // when the Artisan application actually starts up and is getting used. - $this->commands( - 'command.neoeloquent.migrate', - 'command.neoeloquent.migrate.make', - 'command.neoeloquent.migrate.rollback', - 'command.neoeloquent.migrate.reset', - 'command.neoeloquent.migrate.refresh' - ); - } - - /** - * Register the "migrate" migration command. - * - * @return void - */ - protected function registerMigrateCommand() - { - $this->app->singleton('command.neoeloquent.migrate', function($app) { - $packagePath = $app['path.base'].'/vendor'; - - return new MigrateCommand($app['neoeloquent.migrator'], $packagePath); - }); - } - - /** - * Register the "rollback" migration command. - * - * @return void - */ - protected function registerMigrateRollbackCommand() - { - $this->app->singleton('command.neoeloquent.migrate.rollback', function($app) - { - return new MigrateRollbackCommand($app['neoeloquent.migrator']); - }); - } - - /** - * Register the "reset" migration command. - * - * @return void - */ - protected function registerMigrateResetCommand() - { - $this->app->singleton('command.neoeloquent.migrate.reset', function($app) - { - return new MigrateResetCommand($app['neoeloquent.migrator']); - }); - } - - /** - * Register the "refresh" migration command. - * - * @return void - */ - protected function registerMigrateRefreshCommand() - { - $this->app->singleton('command.neoeloquent.migrate.refresh', function($app) - { - return new MigrateRefreshCommand(); - }); - } - - /** - * Register the "install" migration command. - * - * @return void - */ - protected function registerMigrateMakeCommand() - { - $this->app->singleton('migration.neoeloquent.creator', function($app) { - return new MigrationCreator($app['files'], $app->basePath('stubs')); - }); - - $this->app->singleton('command.neoeloquent.migrate.make', function($app) { - // Once we have the migration creator registered, we will create the command - // and inject the creator. The creator is responsible for the actual file - // creation of the migrations, and may be extended by these developers. - $creator = $app['migration.neoeloquent.creator']; - - $packagePath = $app['path.base'].'/vendor'; - - $composer = $app->make('Illuminate\Support\Composer'); - - return new MigrateMakeCommand($creator, $composer, $packagePath); - }); - } - - /** - * {@inheritDoc} - */ - public function provides() - { - return array( - 'neoeloquent.migrator', - 'neoeloquent.migration.repository', - 'command.neoeloquent.migrate', - 'command.neoeloquent.migrate.rollback', - 'command.neoeloquent.migrate.reset', - 'command.neoeloquent.migrate.refresh', - 'migration.neoeloquent.creator', - 'command.neoeloquent.migrate.make', - ); - } - -} diff --git a/src/Migrations/DatabaseMigrationRepository.php b/src/Migrations/DatabaseMigrationRepository.php deleted file mode 100644 index 47203595..00000000 --- a/src/Migrations/DatabaseMigrationRepository.php +++ /dev/null @@ -1,213 +0,0 @@ -resolver = $resolver; - $this->schema = $schema; - $this->model = $model; - } - - /** - * {@inheritDoc} - */ - public function getRan() - { - return $this->model->all()->lists('migration'); - } - - /** - * Get list of migrations. - * - * @param int $steps - * @return array - */ - public function getMigrations($steps) - { - $query = $this->label()->where('batch', '>=', '1'); - - return $query->orderBy('migration', 'desc')->take($steps)->get()->all(); - } - - /** - * {@inheritDoc} - */ - public function getLast() - { - return $this->model->whereBatch($this->getLastBatchNumber())->get()->toArray(); - } - - /** - * {@inheritDoc} - */ - public function log($file, $batch) - { - $record = array('migration' => $file, 'batch' => $batch); - - $this->model->create($record); - } - - /** - * {@inheritDoc} - */ - public function delete($migration) - { - $this->model->where('migration', $migration->migration)->delete(); - } - - /** - * {@inheritDoc} - */ - public function getNextBatchNumber() - { - return $this->getLastBatchNumber() + 1; - } - - /** - * {@inheritDoc} - */ - public function getLastBatchNumber() - { - return $this->label()->max('batch'); - } - - /** - * {@inheritDoc} - */ - public function createRepository() - { - return; - } - - /** - * {@inheritDoc} - */ - public function repositoryExists() - { - return $this->schema->hasLabel($this->getLabel()); - } - - /** - * Get a query builder for the migration node (table). - * - * @return \Vinelab\NeoEloquent\Query\Builder - */ - protected function label() - { - return $this->getConnection()->table(array($this->getLabel())); - } - - /** - * Get the connection resolver instance. - * - * @return \Illuminate\Database\ConnectionResolverInterface - */ - public function getConnectionResolver() - { - return $this->resolver; - } - - /** - * Resolve the database connection instance. - * - * @return \Illuminate\Database\Connection - */ - public function getConnection() - { - return $this->resolver->connection($this->connection); - } - - /** - * {@inheritDoc} - */ - public function setSource($name) - { - $this->connection = $name; - } - - /** - * Set migration models label. - * - * @param string $label - */ - public function setLabel($label) - { - $this->model->setLabel($label); - } - - /** - * Get migration models label. - * - * @return string - */ - public function getLabel() - { - return $this->model->getLabel(); - } - - /** - * Set migration model. - * - * @param \Vinelab\NeoEloquent\Eloquent\Model $model - */ - public function setMigrationModel(Model $model) - { - $this->model = $model; - } - - /** - * Get migration model. - * - * @return \Vinelab\NeoEloquent\Eloquent\Model - */ - public function getMigrationModel() - { - return $this->model; - } - - public function getMigrationBatches() - { - return $this->label()->orderBy('batch') - ->orderBy('migration') - ->get(); - } - - public function deleteRepository(): void - { - $this->label()->delete(); - } -} diff --git a/src/Migrations/Migration.php b/src/Migrations/Migration.php deleted file mode 100644 index fc31801e..00000000 --- a/src/Migrations/Migration.php +++ /dev/null @@ -1,23 +0,0 @@ -connection; - } -} diff --git a/src/Migrations/MigrationCreator.php b/src/Migrations/MigrationCreator.php deleted file mode 100644 index adb467e1..00000000 --- a/src/Migrations/MigrationCreator.php +++ /dev/null @@ -1,39 +0,0 @@ -app['db']); + $this->app->singleton('db.connector.neo4j', ConnectionFactory::class); + + Connection::resolverFor('neo4j', $this->neo4jResolver(...)); - Model::setEventDispatcher($this->app->make(Dispatcher::class)); + $this->registerPercentile('percentileDisc'); + $this->registerPercentile('percentileCont'); + $this->registerAggregate('stdev'); + $this->registerAggregate('stdevp'); + $this->registerCollect(); } - /** - * Register the service provider. - */ - public function register() + private function registerPercentile(string $function): void { - $this->app['db']->extend('neo4j', function ($config) { - $this->config = $config; - $conn = new ConnectionAdapter($config); - $conn->setSchemaGrammar(new CypherGrammar()); - - return $conn; - }); - - $this->app->bind('neoeloquent.connection', function() { - // $config is set by the previous binding, - // so that we get the correct configuration - // set by the user. - $conn = new NeoEloquentConnection($this->config); - $conn->setSchemaGrammar(new CypherGrammar()); - - return $conn; - }); - - $this->app->booting(function () { - $loader = \Illuminate\Foundation\AliasLoader::getInstance(); - $loader->alias('NeoEloquent', 'Vinelab\NeoEloquent\Eloquent\Model'); - $loader->alias('Neo4jSchema', 'Vinelab\NeoEloquent\Facade\Neo4jSchema'); - $loader->alias('Illuminate\Database\Eloquent\Factory', 'Vinelab\NeoEloquent\Eloquent\NeoEloquentFactory'); - }); - - $this->app->singleton(NeoEloquentFactory::class, function ($app) { - return NeoEloquentFactory::construct( - $app->make(FakerGenerator::class), $this->app->databasePath('factories') - ); - }); - - $this->registerComponents(); + $macro = function (string $logins, float|int $percentile = null) use ($function): float { + /** @var \Vinelab\NeoEloquent\Query\Builder $x */ + $x = $this; + + return $x->aggregate($function, [$logins, new RawExpression((string) ($percentile ?? 0.0))]); + }; + + Builder::macro($function, $macro); + \Illuminate\Database\Eloquent\Builder::macro($function, $macro); } - /** - * Register components on the provider. - * - * @var array - */ - protected function registerComponents() + private function registerAggregate(string $functionName): void { - foreach ($this->components as $component) { - $this->{'register'.$component}(); - } + $macro = function (string $logins) use ($functionName): mixed { + /** @var \Vinelab\NeoEloquent\Query\Builder $x */ + $x = $this; + + return $x->aggregate($functionName, [$logins]); + }; + + Builder::macro($functionName, $macro); + \Illuminate\Database\Eloquent\Builder::macro($functionName, $macro); } - /** - * Register the migration service provider. - */ - protected function registerMigration() + private function registerCollect(): void { - $this->app->register(MigrationServiceProvider::class); + $macro = function (string $logins): Collection { + /** @var \Vinelab\NeoEloquent\Query\Builder $x */ + $x = $this; + + return new Collection($x->aggregate('collect', [$logins])->toArray()); + }; + + Builder::macro('collect', $macro); + \Illuminate\Database\Eloquent\Builder::macro('collect', $macro); } /** - * Get the services provided by the provider. - * - * @return array + * @param callable():Driver $driver */ - public function provides() + private function neo4jResolver(callable $driver, string $database, string $prefix, array $config): Connection { - return array( + $sessionConfig = SessionConfiguration::default() + ->withDatabase($config['database'] ?? null); + + $driver = $driver(); + + return new \Vinelab\NeoEloquent\Connection( + $driver->createSession($sessionConfig->withAccessMode(AccessMode::READ())), + $driver->createSession($sessionConfig), + $database, + $prefix, + $config ); } } diff --git a/src/Processors/Processor.php b/src/Processors/Processor.php new file mode 100644 index 00000000..9239a26e --- /dev/null +++ b/src/Processors/Processor.php @@ -0,0 +1,113 @@ +from ?? $builder->table; + } elseif ($builder instanceof Builder) { + $from = $builder->from; + } else { + $from = $builder; + } + + if (str_contains($from, ' as ')) { + [$label, $name] = explode(' as ', $from, 2); + } else { + $label = $from; + } + + if (str_starts_with($label, '<') || str_ends_with($label, '>')) { + $target = 'relationship'; + } else { + $target = 'node'; + } + + [$labelOrType, $name] = (new GraphPattern())->decode($label, $target, $direction, $name); + + return [$labelOrType[0], $name, $target === 'relationship', $direction]; + } + + public static function standardiseColumn(string $column): string + { + if (! str_contains($column, '.')) { + return $column; + } + + [$table, $column] = explode('.', $column, 2); + [1 => $name] = Processor::fromToName($table); + + return "$name.$column"; + } + + public function processSelect(Builder $query, $results): array + { + $tbr = []; + [1 => $from] = self::fromToName($query); + foreach ($results as $row) { + $processedRow = []; + + foreach ($row as $key => $value) { + if ($value instanceof HasPropertiesInterface) { + $preface = $key.'.'; + if ($key === $from) { + $preface = ''; + } + + foreach ($value->getProperties() as $prop => $x) { + $processedRow[$preface.$prop] = $this->filterDateTime($x); + } + } else { + $processedRow[$key] = $this->filterDateTime($value); + } + } + $tbr[] = $processedRow; + } + + return $tbr; + } + + public function processInsertGetId(Builder $query, $sql, $values, $sequence = null): mixed + { + $prop = Arr::first($query->getConnection()->selectOne($sql, $values, false)); + if (is_string($sequence) && $prop instanceof HasPropertiesInterface) { + return $prop->getProperties()->get($sequence); + } + + return $prop; + } + + private function filterDateTime(mixed $x): mixed + { + if ($x instanceof DateTimeZoneId || $x instanceof DateTime) { + return $x->toDateTime(); + } + + return $x; + } + + public function processColumnListing($results): array + { + return Arr::pluck($results, 'column_name'); + } +} diff --git a/src/Query/Adapter/IlluminateToQueryStructurePipeline.php b/src/Query/Adapter/IlluminateToQueryStructurePipeline.php new file mode 100644 index 00000000..51341bd0 --- /dev/null +++ b/src/Query/Adapter/IlluminateToQueryStructurePipeline.php @@ -0,0 +1,206 @@ +|null */ + private static WeakMap|null $cache = null; + + /** + * @param list $decorators + */ + private function __construct( + private readonly array $decorators, + private readonly ParameterStack $parameterStack + ) { + } + + /** + * @return array + */ + public static function getBindings(Builder $builder): array + { + $bindings = self::getCache()[$builder] ?? null; + + if ($bindings === null) { + return []; + } + + /** + * @psalm-suppress InternalProperty + * @psalm-suppress InternalMethod + */ + return array_map(static fn (Parameter $x): mixed => $x->value, $bindings->getStructure()->parameters->getParameters()); + } + + public function pipe(Builder $illuminateBuilder): QueryBuilder + { + + if ($object = Tracer::isInBelongsToManyWithRelationship($illuminateBuilder)) { + $parent = $object->getParent(); + $related = $object->getRelated(); + + [0 => $labelOrType, 1 => $name, 3 => $direction] = Processor::fromToName($object->getTable()); + + [$parentLabelOrType, $parentName] = Processor::fromToName($parent->getTable()); + [$relatedLabelOrType, $relatedName] = Processor::fromToName($related->getTable()); + + $patterns = GraphPatternBuilder::fromNode($parentLabelOrType, $parentName) + ->addRelationship($labelOrType, $name, $direction) + ->addChildNode($relatedLabelOrType, $relatedName) + ->end() + ->end(); + } else { + [$labelOrType, $name, $isRelationship, $direction] = Processor::fromToName($illuminateBuilder); + + if ($isRelationship) { + $patterns = GraphPatternBuilder::fromRelationship($labelOrType, $name, $direction, $this->containsLeftJoin($illuminateBuilder)); + } else { + $patterns = GraphPatternBuilder::fromNode($labelOrType, $name, $this->containsLeftJoin($illuminateBuilder)); + } + + $this->decorateBuilder($illuminateBuilder, $patterns); + } + + $builder = QueryBuilder::from($patterns); + + foreach ($this->parameterStack as $key => $value) { + $builder->getStructure()->parameters->add($value, $key); + } + + foreach ($this->decorators as $decorator) { + $decorator->decorate($illuminateBuilder, $builder); + } + + self::getCache()[$illuminateBuilder] = $builder; + + foreach ($builder->getStructure()->parameters as $key => $value) { + $this->parameterStack->add($value, $key); + } + + return $builder; + } + + private function decorateBuilder(Builder $builder, PatternBuilder $patternBuilder): void + { + /** + * @psalm-suppress RedundantConditionGivenDocblockType + * @psalm-suppress DocblockTypeContradiction + * + * @var JoinClause $join + */ + foreach (($builder->joins ?? []) as $join) { + [$labelOrType, $name, $isRelationship, $direction] = Processor::fromToName($join); + $optional = $join->type === 'left'; + + if ($isRelationship) { + $child = $patternBuilder->addRelationship($labelOrType, $name, $direction, $optional); + } else { + $child = $patternBuilder->addChildNode($labelOrType, $name, $optional); + } + + $this->decorateBuilder($join, $patternBuilder); + + $child->end(); + } + } + + private function containsLeftJoin(Builder $builder): bool + { + /** + * @psalm-suppress RedundantConditionGivenDocblockType + * @psalm-suppress DocblockTypeContradiction + */ + foreach (($builder->joins ?? []) as $join) { + if ($join->type === 'left') { + return true; + } + } + + return false; + } + + public static function create(ParameterStack|null $stack = null): self + { + return new self([], $stack ?? new ParameterStack()); + } + + public function withWheres(): self + { + return new self([...$this->decorators, ...[new IlluminateToWhereDecorator()]], $this->parameterStack); + } + + public function withReturn(): self + { + return new self([...$this->decorators, ...[new IlluminateToReturnDecorator()]], $this->parameterStack); + } + + public function withCreate(array $values): self + { + return new self([...$this->decorators, ...[new IlluminateToCreatingDecorating($values, false)]], $this->parameterStack); + } + + public function withBatchCreate(array $values): self + { + return new self([...$this->decorators, ...[new IlluminateToCreatingDecorating($values, true)]], $this->parameterStack); + } + + public function withSet(array $values): self + { + return new self([...$this->decorators, ...[new IlluminateToSettingDecorator($values)]], $this->parameterStack); + } + + public function withMerge(array $values, array $uniqueBy, array $update): self + { + return new self([...$this->decorators, ...[new IlluminateToMergeDecorator($values, $uniqueBy, $update)]], $this->parameterStack); + } + + public function withParameterStack(ParameterStack $stack): self + { + return new self($this->decorators, $stack); + } + + public function withDelete(): self + { + return new self([...$this->decorators, ...[new IlluminateToDeletingDecorator()]], $this->parameterStack); + } + + public function withUnion(): self + { + return new self([...$this->decorators, ...[new IlluminateToUnioningDecorator( + static fn () => IlluminateToQueryStructurePipeline::create()->withWheres()->withReturn() + )]], $this->parameterStack); + } + + /** + * @return WeakMap + */ + private static function getCache(): WeakMap + { + if (self::$cache === null) { + /** @var WeakMap */ + self::$cache = new WeakMap(); + } + + return self::$cache; + } +} diff --git a/src/Query/Adapter/Partial/IlluminateToCreatingDecorating.php b/src/Query/Adapter/Partial/IlluminateToCreatingDecorating.php new file mode 100644 index 00000000..209a6129 --- /dev/null +++ b/src/Query/Adapter/Partial/IlluminateToCreatingDecorating.php @@ -0,0 +1,82 @@ +values; + + // First, we detect if this method is delegated by a BelongsToMany relationship. + // We must also make sure there are actual nodes being created. + // We will connect the parent and related nodes with the relationship. + $object = Tracer::isInBelongsToManyWithRelationship($illuminateBuilder); + if (count($values) > 0 && $object !== null) { + + // In this case we will not create another node, but rather connect the two nodes with a relationship. + // The problem here is that we will create an Unwind loop and match the parent and related nodes for + // each connection. + $qualifiedParentKeyName = $object->getQualifiedParentKeyName(); + $qualifiedRelatedKeyName = $object->getQualifiedRelatedKeyName(); + + $cypherBuilder->whereEquals($qualifiedParentKeyName, new RawExpression('toCreate["parent"]')) + ->andWhereEquals($qualifiedRelatedKeyName, new RawExpression('toCreate["related"]')); + + $creating = []; + + // Because the connection is now embedded into the relationship, we do not need + // to embed the keys into the properties of the relationship itself anymore. + foreach ($values as &$row) { + $toCreate = []; + $toCreate['parent'] = $row[$object->getForeignPivotKeyName()]; + $toCreate['related'] = $row[$object->getRelatedPivotKeyName()]; + + // unset($row[$object->getForeignPivotKeyName()]); + // unset($row[$object->getRelatedPivotKeyName()]); + + $toCreate['values'] = $row; + $creating[] = $toCreate; + } + + [1 => $name] = Processor::fromToName($illuminateBuilder); + + foreach (array_keys($values[0]) as $column) { + $original = $column; + if (! str_contains($column, '.')) { + $column = "$name.$column"; + } + + $cypherBuilder->creating([ + Processor::standardiseColumn($column) => new RawExpression("toCreate['values']['$original']"), + ]); + } + + $cypherBuilder->getStructure()->parameters->add($creating, 'toCreate'); + + return; + } + + if ($this->batch) { + /** @psalm-suppress ArgumentTypeCoercion */ + $cypherBuilder->batchCreating($values); + } else { + $cypherBuilder->creating($values); + } + } +} diff --git a/src/Query/Adapter/Partial/IlluminateToDeletingDecorator.php b/src/Query/Adapter/Partial/IlluminateToDeletingDecorator.php new file mode 100644 index 00000000..9e92ba67 --- /dev/null +++ b/src/Query/Adapter/Partial/IlluminateToDeletingDecorator.php @@ -0,0 +1,28 @@ +getStructure()->graphPattern->chunk('match'); + foreach ($parts as $part) { + if (! $part instanceof RawExpression) { + $cypherBuilder->deleting($part->name->name); + } + } + } +} diff --git a/src/Query/Adapter/Partial/IlluminateToMergeDecorator.php b/src/Query/Adapter/Partial/IlluminateToMergeDecorator.php new file mode 100644 index 00000000..cc62d507 --- /dev/null +++ b/src/Query/Adapter/Partial/IlluminateToMergeDecorator.php @@ -0,0 +1,24 @@ +merging($this->uniqueBy) + ->onCreating($this->values) + ->onMatching($this->update); + } +} diff --git a/src/Query/Adapter/Partial/IlluminateToReturnDecorator.php b/src/Query/Adapter/Partial/IlluminateToReturnDecorator.php new file mode 100644 index 00000000..bae3d68a --- /dev/null +++ b/src/Query/Adapter/Partial/IlluminateToReturnDecorator.php @@ -0,0 +1,85 @@ +aggregate; + if ($aggregate) { + if (in_array('*', $aggregate['columns'])) { + $aggregate['columns'] = [new RawExpression('*')]; + } + + if ($illuminateBuilder->distinct) { + $aggregate['columns'] = [new RawExpression('DISTINCT'), ...$aggregate['columns']]; + } + + $cypherBuilder->returningProcedure($aggregate['function'], 'aggregate', ...$aggregate['columns']); + + return; + } + + $columns = array_merge($illuminateBuilder->columns ?? [], $illuminateBuilder->groups ?? []); + + if ($illuminateBuilder->limit !== null) { + $cypherBuilder->limiting($illuminateBuilder->limit); + } + + if ($illuminateBuilder->offset !== null) { + $cypherBuilder->skipping($illuminateBuilder->offset); + } + + $direction = 'asc'; + $first = Arr::first($illuminateBuilder->orders); + if (is_array($first)) { + $direction = $first['direction']; + } + $direction = Str::upper($direction); + $orders = array_merge(Arr::pluck($illuminateBuilder->orders ?? [], 'column'), $illuminateBuilder->groups ?? []); + + if (count($orders) > 0) { + /** @psalm-suppress ArgumentTypeCoercion */ + $cypherBuilder->orderingBy($direction, ...$orders); + } + + $distinct = $illuminateBuilder->distinct; + $cypherBuilder->distinct(is_bool($distinct) ? $distinct : count($distinct) > 0); + + if (count($columns) > 0) { + $usedRaw = false; + foreach ($columns as $column) { + if ($column instanceof Expression) { + $column = $column->getValue(new CypherGrammar()); + } + if (! $usedRaw && str_contains($column, '*')) { + $cypherBuilder->returningRaw('*'); + $usedRaw = true; + } else { + $cypherBuilder->returning(Processor::standardiseColumn($column)); + } + } + } else { + $cypherBuilder->returningAll(); + } + } +} diff --git a/src/Query/Adapter/Partial/IlluminateToSettingDecorator.php b/src/Query/Adapter/Partial/IlluminateToSettingDecorator.php new file mode 100644 index 00000000..42400807 --- /dev/null +++ b/src/Query/Adapter/Partial/IlluminateToSettingDecorator.php @@ -0,0 +1,23 @@ +values as $property => $value) { + $cypherBuilder->setting($property, $value); + } + } +} diff --git a/src/Query/Adapter/Partial/IlluminateToUnioningDecorator.php b/src/Query/Adapter/Partial/IlluminateToUnioningDecorator.php new file mode 100644 index 00000000..6879ae2c --- /dev/null +++ b/src/Query/Adapter/Partial/IlluminateToUnioningDecorator.php @@ -0,0 +1,36 @@ +unions) === 0) { + return; + } + + $pipeline = call_user_func($this->pipeline); + + foreach ($illuminateBuilder->unions as $union) { + $cypherBuilder->unioning($pipeline->pipe($union)); + } + } +} diff --git a/src/Query/Adapter/Partial/IlluminateToWhereDecorator.php b/src/Query/Adapter/Partial/IlluminateToWhereDecorator.php new file mode 100644 index 00000000..e6db1061 --- /dev/null +++ b/src/Query/Adapter/Partial/IlluminateToWhereDecorator.php @@ -0,0 +1,326 @@ +} + * @psalm-type WhereColumns = WhereCommonArray&array{columns: list, operator: string, values: list} + * @psalm-type WhereRawArray = WhereCommonArray&array{sql: string} + * + * @psalm-suppress ArgumentTypeCoercion + */ +class IlluminateToWhereDecorator implements IlluminateToQueryStructureDecorator +{ + private function where(Builder $builder, array $where, SubQueryBuilder $cypherBuilder): void + { + /** @psalm-suppress InternalProperty */ + $stack = $cypherBuilder->getStructure()->parameters; + + $method = Str::camel($where['type']); + + match ($method) { + 'raw' => $this->raw($where, $cypherBuilder), + 'basic' => $this->basic($builder, $where, $cypherBuilder), + 'in' => $this->in($builder, $where, $cypherBuilder), + 'notIn' => $this->notIn($builder, $where, $cypherBuilder), + 'inRaw' => $this->inRaw($builder, $where, $cypherBuilder), + 'notInRaw' => $this->notInRaw($builder, $where, $cypherBuilder), + 'null' => $this->null($builder, $where, $cypherBuilder), + 'notNull' => $this->notNull($builder, $where, $cypherBuilder), + 'rowValues' => $this->rowValues($builder, $where, $cypherBuilder), + 'notExists' => $this->notExists($builder, $where, $cypherBuilder), + 'exists' => $this->exists($where, $cypherBuilder), + 'count' => $this->count($where, $cypherBuilder), + 'year' => $this->year($builder, $where, $cypherBuilder, $stack), + 'month' => $this->month($builder, $where, $cypherBuilder, $stack), + 'day' => $this->day($builder, $where, $cypherBuilder, $stack), + 'time' => $this->time($builder, $where, $cypherBuilder, $stack), + 'date' => $this->date($builder, $where, $cypherBuilder, $stack), + 'column' => $this->column($builder, $where, $cypherBuilder), + 'betweenColumn' => $this->betweenColumn($builder, $where, $cypherBuilder), + 'between' => $this->between($builder, $where, $cypherBuilder), + 'nested' => $this->nested($where, $cypherBuilder), + }; + } + + /** + * @param WhereRawArray $where + */ + private function raw(array $where, WhereBuilder $cypherBuilder): void + { + $cypherBuilder->whereRaw($where['sql'], $this->compileBoolean($where['boolean'])); + } + + /** + * @param WhereBasicArray $where + */ + private function basic(Builder $builder, array $where, WhereBuilder $cypherBuilder): void + { + $cypherBuilder->where( + Processor::standardiseColumn($where['column']), + $where['operator'], + $where['value'], + $this->compileBoolean($where['boolean']) + ); + } + + /** + * @param WhereColumnInArray $where + */ + private function in(Builder $builder, array $where, WhereBuilder $cypherBuilder): void + { + $where['value'] = $where['values']; + + $where['operator'] = 'in'; + + $this->basic($builder, $where, $cypherBuilder); + } + + /** + * @param WhereColumnInArray $where + */ + private function notIn(Builder $builder, array $where, WhereBuilder $cypherBuilder): void + { + $cypherBuilder->whereNot(function (SubQueryBuilder $cypherBuilder) use ($builder, $where) { + $this->in($builder, $where, $cypherBuilder); + }, $this->compileBoolean($where['boolean'])); + } + + /** + * @param WhereColumnInArray $where + */ + private function inRaw(Builder $builder, array $where, WhereBuilder $cypherBuilder): void + { + $this->in($builder, $where, $cypherBuilder); + } + + /** + * @param WhereColumnInArray $where + */ + private function notInRaw(Builder $builder, array $where, WhereBuilder $cypherBuilder): void + { + $cypherBuilder->whereNot(function (SubQueryBuilder $cypherBuilder) use ($builder, $where) { + $this->inRaw($builder, $where, $cypherBuilder); + }, $this->compileBoolean($where['boolean'])); + } + + /** + * @param WhereColumnArray $where + */ + private function null(Builder $builder, array $where, WhereBuilder $cypherBuilder): void + { + $cypherBuilder->whereNull(Processor::standardiseColumn($where['column']), $this->compileBoolean($where['boolean'])); + } + + private function notNull(Builder $builder, array $where, WhereBuilder $cypherBuilder): void + { + $cypherBuilder->whereNot(function (SubQueryBuilder $cypherBuilder) use ($builder, $where) { + $this->null($builder, $where, $cypherBuilder); + }, $this->compileBoolean($where['boolean'])); + } + + /** + * @param WhereColumns $where + */ + private function rowValues(Builder $builder, array $where, WhereBuilder $cypherBuilder): void + { + foreach ($where['columns'] as $i => $column) { + $cypherBuilder->whereEquals(Processor::standardiseColumn($column), $where['values'][$i], BooleanOperator::AND); + } + } + + private function notExists(Builder $builder, array $where, WhereBuilder $cypherBuilder): void + { + $cypherBuilder->whereNot(function (SubQueryBuilder $cypherBuilder) use ($where) { + $this->exists($where, $cypherBuilder); + }, $where['boolean']); + } + + private function exists(array $where, WhereBuilder $cypherBuilder): void + { + $builder = IlluminateToQueryStructurePipeline::create() + ->withParameterStack($cypherBuilder->getStructure()->parameters) + ->withWheres() + ->pipe($where['query']); + + $cypherBuilder->whereExists($builder, $this->compileBoolean($where['boolean'])); + } + + private function count(array $where, WhereBuilder $cypherBuilder): void + { + $builder = IlluminateToQueryStructurePipeline::create() + ->withWheres() + ->pipe($where['query']); + + $cypherBuilder->whereCount($builder, $where['value'], '>=', $this->compileBoolean($where['boolean'])); + } + + /** + * @param WhereBasicArray $where + */ + private function year(Builder $builder, array $where, WhereBuilder $cypherBuilder, ParameterStack $stack): void + { + $this->temporal($builder, $where, $cypherBuilder, 'year', $stack); + } + + /** + * @param WhereBasicArray $where + */ + private function temporal(Builder $builder, array $where, WhereBuilder $cypherBuilder, string $attribute, ParameterStack $stack): void + { + $cypherBuilder->where( + Processor::standardiseColumn($where['column']), + '=', + new RawExpression('$'.$stack->add($where['value'])->name.'.'.$attribute), + $this->compileBoolean($where['boolean']) + ); + } + + /** + * @param WhereBasicArray $where + */ + private function month(Builder $builder, array $where, WhereBuilder $cypherBuilder, ParameterStack $stack): void + { + $this->temporal($builder, $where, $cypherBuilder, 'month', $stack); + } + + /** + * @param WhereBasicArray $where + */ + private function day(Builder $builder, array $where, WhereBuilder $cypherBuilder, ParameterStack $stack): void + { + $this->temporal($builder, $where, $cypherBuilder, 'day', $stack); + } + + /** + * @param WhereBasicArray $where + */ + private function time(Builder $builder, array $where, WhereBuilder $cypherBuilder, ParameterStack $stack): void + { + $this->temporal($builder, $where, $cypherBuilder, 'time', $stack); + } + + /** + * @param WhereBasicArray $where + */ + private function date(Builder $builder, array $where, WhereBuilder $cypherBuilder, ParameterStack $stack): void + { + $this->temporal($builder, $where, $cypherBuilder, 'date', $stack); + } + + /** + * @param WhereBasicColumnArray $where + */ + private function column(Builder $builder, array $where, WhereBuilder $cypherBuilder): void + { + // Workaround in a very obscure bug when using deeply nested WHERE EXISTS subqueries. + if (str_starts_with($where['second'], $builder->from) && $where['operator'] === '=') { + $tmp = $where['first']; + $where['first'] = $where['second']; + $where['second'] = $tmp; + } + + $cypherBuilder->wherePropertiesEquals( + Processor::standardiseColumn($where['first']), + Processor::standardiseColumn($where['second']), + $this->compileBoolean($where['boolean']) + ); + } + + /** + * @param WhereColumns $where + */ + private function betweenColumn(Builder $builder, array $where, SubQueryBuilder $cypherBuilder): void + { + $callable = function (SubQueryBuilder $cypherBuilder) use ($builder, $where): void { + $first = reset($where['values']); + $second = end($where['values']); + + $where['first'] = $where['column']; + $where['second'] = $first; + $this->column($builder, $where, $cypherBuilder); + + $where['second'] = $second; + $this->column($builder, $where, $cypherBuilder); + }; + + if ($where['not']) { + $cypherBuilder->whereNot($callable); + } else { + $callable($cypherBuilder); + } + } + + /** + * @param WhereColumnArray $where + */ + private function between(Builder $builder, array $where, SubQueryBuilder $cypherBuilder): void + { + $callable = function (SubQueryBuilder $cypherBuilder) use ($builder, $where): void { + $first = reset($where['values']); + $second = end($where['values']); + + $where['operator'] = '='; + $where['value'] = $first; + $this->basic($builder, $where, $cypherBuilder); + + $where['value'] = $second; + $this->basic($builder, $where, $cypherBuilder); + }; + + if ($where['not']) { + $cypherBuilder->whereNot($callable); + } else { + $callable($cypherBuilder); + } + } + + private function nested(array $where, WhereBuilder $cypherBuilder): void + { + $sub = IlluminateToQueryStructurePipeline::create() + ->withParameterStack($cypherBuilder->getStructure()->parameters) + ->withWheres() + ->pipe($where['query']); + + $cypherBuilder->whereInner($sub, $this->compileBoolean($where['boolean'])); + } + + private function compileBoolean(string $operator): BooleanOperator + { + return match (strtolower($operator)) { + 'and' => BooleanOperator::AND, + 'or' => BooleanOperator::OR, + 'xor' => BooleanOperator::XOR + }; + } + + public function decorate(Builder $illuminateBuilder, SubQueryBuilder $cypherBuilder): void + { + /** @psalm-suppress RedundantConditionGivenDocblockType */ + foreach (array_merge($illuminateBuilder->wheres, ($illuminateBuilder->havings ?? [])) as $where) { + $this->where($illuminateBuilder, $where, $cypherBuilder); + } + + /** @psalm-suppress RedundantConditionGivenDocblockType */ + foreach (($illuminateBuilder->joins ?? []) as $join) { + $this->decorate($join, $cypherBuilder); + } + } +} diff --git a/src/Query/Adapter/Tracer.php b/src/Query/Adapter/Tracer.php new file mode 100644 index 00000000..a4e13a81 --- /dev/null +++ b/src/Query/Adapter/Tracer.php @@ -0,0 +1,35 @@ + $isRelationship] = Processor::fromToName($builder); + if (! $isRelationship && is_array($builder->joins) && count($builder->joins) === 1) { + [2 => $isRelationship] = Processor::fromToName($builder->joins[0]->table); + } + + if ($object instanceof BelongsToMany && $isRelationship) { + return $object; + } + + return null; + } +} diff --git a/src/Query/Builder.php b/src/Query/Builder.php index dac2202a..8b43d87b 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -2,2441 +2,69 @@ namespace Vinelab\NeoEloquent\Query; -use Closure; -use DateTime; -use Carbon\Carbon; -use BadMethodCallException; -use InvalidArgumentException; -use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\Node; -use Vinelab\NeoEloquent\ConnectionInterface; -use GraphAware\Common\Result\AbstractRecordCursor as Result; -use Vinelab\NeoEloquent\Eloquent\Collection; -use Vinelab\NeoEloquent\Query\Grammars\Grammar; +use function array_key_exists; +use function compact; +use function debug_backtrace; +use const DEBUG_BACKTRACE_PROVIDE_OBJECT; +use Illuminate\Database\Query\Expression; -use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Support\Arr; -use Illuminate\Support\Str; -use Illuminate\Pagination\LengthAwarePaginator; -use Illuminate\Pagination\Paginator; -use Vinelab\NeoEloquent\Traits\ResultTrait; - -class Builder +class Builder extends \Illuminate\Database\Query\Builder { - use ResultTrait; - - /** - * The database connection instance. - * - * @var Vinelab\NeoEloquent\Connection - */ - protected $connection; - - /** - * The database active client handler. - * - * @var Neoxygen\NeoClient\Client - */ - protected $client; - - /** - * The database query grammar instance. - * - * @var \Vinelab\NeoEloquent\Query\Grammars\Grammar - */ - protected $grammar; - - /** - * The database query post processor instance. - * - * @var \Vinelab\NeoEloquent\Query\Processors\Processor - */ - protected $processor; - - /** - * The matches constraints for the query. - * - * @var array - */ - public $matches = array(); - - /** - * The WITH parts of the query. - * - * @var array - */ - public $with = array(); - - /** - * The current query value bindings. - * - * @var array - */ - protected $bindings = array( - 'matches' => [], - 'select' => [], - 'join' => [], - 'where' => [], - 'having' => [], - 'order' => [], - ); - - /** - * All of the available clause operators. - * - * @var array - */ - protected $operators = array( - '+', '-', '*', '/', '%', '^', // Mathematical - '=', '<>', '<', '>', '<=', '>=', // Comparison - 'is null', 'is not null', - 'and', 'or', 'xor', 'not', // Boolean - 'in', '[x]', '[x .. y]', // Collection - '=~', // Regular Expression - ); - - /** - * An aggregate function and column to be run. - * - * @var array - */ - public $aggregate; - - /** - * The columns that should be returned. - * - * @var array - */ - public $columns; - - /** - * Indicates if the query returns distinct results. - * - * @var bool - */ - public $distinct = false; - - /** - * The table which the query is targeting. - * - * @var string - */ - public $from; - - /** - * The where constraints for the query. - * - * @var array - */ - public $wheres; - - /** - * The groupings for the query. - * - * @var array - */ - public $groups; - - /** - * The having constraints for the query. - * - * @var array - */ - public $havings; - - /** - * The orderings for the query. - * - * @var array - */ - public $orders; - - /** - * The maximum number of records to return. - * - * @var int - */ - public $limit; - - /** - * The number of records to skip. - * - * @var int - */ - public $offset; - - /** - * The query union statements. - * - * @var array - */ - public $unions; - - /** - * The maximum number of union records to return. - * - * @var int - */ - public $unionLimit; - - /** - * The number of union records to skip. - * - * @var int - */ - public $unionOffset; - - /** - * The orderings for the union query. - * - * @var array - */ - public $unionOrders; - - /** - * Indicates whether row locking is being used. - * - * @var string|bool - */ - public $lock; - - /** - * The field backups currently in use. - * - * @var array - */ - protected $backups = []; - - /** - * The binding backups currently in use. - * - * @var array - */ - protected $bindingBackups = []; - - /** - * Create a new query builder instance. - * - * @param Vinelab\NeoEloquent\Connection $connection - */ - public function __construct(ConnectionInterface $connection, Grammar $grammar) - { - $this->grammar = $grammar; - $this->grammar->setQuery($this); - - $this->connection = $connection; - - $this->client = $connection->getClient(); - } - - /** - * Set the columns to be selected. - * - * @param array $columns - * - * @return $this - */ - public function select($columns = ['*']) - { - $this->columns = is_array($columns) ? $columns : func_get_args(); - - return $this; - } - - /** - * Add a new "raw" select expression to the query. - * - * @param string $expression - * @param array $bindings - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function selectRaw($expression, array $bindings = []) - { - $this->addSelect(new Expression($expression)); - - if ($bindings) { - $this->addBinding($bindings, 'select'); - } - - return $this; - } - - /** - * Add a subselect expression to the query. - * - * @param \Closure|\Vinelab\NeoEloquent\Query\Builder|string $query - * @param string $as - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function selectSub($query, $as) - { - if ($query instanceof Closure) { - $callback = $query; - - $callback($query = $this->newQuery()); - } - - if ($query instanceof self) { - $bindings = $query->getBindings(); + public function where($column, $operator = null, $value = null, $boolean = 'and'): self + { + // Horrible hack to make sure the where Count query does not become + // a sql expression hard coded into the laravel query builder. + if ($column instanceof Expression) { + $stack = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 3); + if (array_key_exists(2, $stack) && $stack[2]['function'] === 'addWhereCountQuery' && isset($stack[2]['args'])) { + $query = $stack[2]['args'][0]; + $value = $stack[2]['args'][2]; + $operator = $stack[2]['args'][1]; + $boolean = $stack[2]['args'][3]; + + $type = 'Count'; + $this->wheres[] = compact('type', 'query', 'operator', 'value', 'boolean'); + $this->addBinding($query->getBindings(), 'where'); + } - $query = $query->toCypher(); - } elseif (is_string($query)) { - $bindings = []; - } else { - throw new InvalidArgumentException(); + return $this; } - return $this->selectRaw('('.$query.') as '.$this->grammar->wrap($as), $bindings); - } - - /** - * Add a new select column to the query. - * - * @param mixed $column - * - * @return $this - */ - public function addSelect($column) - { - $column = is_array($column) ? $column : func_get_args(); - - $this->columns = array_merge((array) $this->columns, $column); - - return $this; + return parent::where($column, $operator, $value, $boolean); // TODO: Change the autogenerated stub } /** - * Force the query to only return distinct results. + * Add a sub-query count clause to this query. * + * @param \Illuminate\Database\Query\Builder $query * @return $this */ - public function distinct() - { - $this->distinct = true; - - return $this; - } - - /** - * Set the node's label which the query is targeting. - * - * @param string $label - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function from($label) + protected function addWhereCountQuery(\Illuminate\Contracts\Database\Query\Builder $query, string $operator = '>=', int $count = 1, string $boolean = 'and'): static { - $this->from = $label; + $type = 'Count'; + $value = $count; + $this->wheres[] = compact('type', 'query', 'operator', 'value', 'boolean'); return $this; } - /** - * Insert a new record and get the value of the primary key. - * - * @param array $values - * @param string $sequence - * - * @return int - */ - public function insertGetId(array $values, $sequence = null) - { - $cypher = $this->grammar->compileCreate($this, $values); - - $bindings = $this->getBindingsMergedWithValues($values); - - /** @var CypherList $results */ - $results = $this->connection->insert($cypher, $bindings); - - /** @var Node $node */ - $node = $results->first()->first()->getValue(); - return $node->getId(); - } - - /** - * Update a record in the database. - * - * @param array $values - * - * @return int - */ - public function update(array $values) - { - $cypher = $this->grammar->compileUpdate($this, $values); - - $bindings = $this->getBindingsMergedWithValues($values, true); - - $updated = $this->connection->update($cypher, $bindings); - - return ($updated) ? count(current($this->getRecordsByPlaceholders($updated))) : 0; - } - - /** - * Bindings should have the keys postfixed with _update as used - * in the CypherGrammar so that we differentiate them from - * query bindings avoiding clashing values. - * - * @param array $values - * - * @return array - */ - protected function getBindingsMergedWithValues(array $values, $updating = false) - { - $bindings = []; - - $values = $this->getGrammar()->postfixValues($values, $updating); - - foreach ($values as $key => $value) { - $bindings[$key] = $value; - } - - return array_merge($this->getBindings(), $bindings); - } - - /** - * Get the current query value bindings in a flattened array - * of $key => $value. - * - * @return array - */ - public function getBindings() + public function getBindings(): array { - $bindings = []; - - // We will run through all the bindings and pluck out - // the component (select, where, etc.) - foreach ($this->bindings as $component => $binding) { - if (!empty($binding)) { - // For every binding there could be multiple - // values set so we need to add all of them as - // flat $key => $value item in our $bindings. - foreach ($binding as $key => $value) { - $bindings[$key] = $value; - } - } - } - - return $bindings; + // bindings are taken care of at the grammar level + return []; } - /** - * Add a basic where clause to the query. - * - * @param string $column - * @param string $operator - * @param mixed $value - * @param string $boolean - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - * - * @throws \InvalidArgumentException - */ - public function where($column, $operator = null, $value = null, $boolean = 'and') + public function addBinding($value, $type = 'where'): Builder { - // First we check whether the operator is 'IN' so that we call whereIn() on it - // as a helping hand and centralization strategy, whereIn knows what to do with the IN operator. - if (mb_strtolower($operator) == 'in') { - return $this->whereIn($column, $value, $boolean); - } - - // If the column is an array, we will assume it is an array of key-value pairs - // and can add them each as a where clause. We will maintain the boolean we - // received when the method was called and pass it into the nested where. - if (is_array($column)) { - return $this->whereNested(function (self $query) use ($column) { - foreach ($column as $key => $value) { - $query->where($key, '=', $value); - } - }, $boolean); - } - - if (func_num_args() == 2) { - list($value, $operator) = array($operator, '='); - } elseif ($this->invalidOperatorAndValue($operator, $value)) { - throw new \InvalidArgumentException('Value must be provided.'); - } - - // If the columns is actually a Closure instance, we will assume the developer - // wants to begin a nested where statement which is wrapped in parenthesis. - // We'll add that Closure to the query then return back out immediately. - if ($column instanceof Closure) { - return $this->whereNested($column, $boolean); - } - - // If the given operator is not found in the list of valid operators we will - // assume that the developer is just short-cutting the '=' operators and - // we will set the operators to '=' and set the values appropriately. - if (!in_array(mb_strtolower($operator), $this->operators, true)) { - list($value, $operator) = array($operator, '='); - } - - // If the value is a Closure, it means the developer is performing an entire - // sub-select within the query and we will need to compile the sub-select - // within the where clause to get the appropriate query record results. - if ($value instanceof Closure) { - return $this->whereSub($column, $operator, $value, $boolean); - } - - // If the value is "null", we will just assume the developer wants to add a - // where null clause to the query. So, we will allow a short-cut here to - // that method for convenience so the developer doesn't have to check. - if (is_null($value)) { - return $this->whereNull($column, $boolean, $operator != '='); - } - - // Now that we are working with just a simple query we can put the elements - // in our array and add the query binding to our array of bindings that - // will be bound to each SQL statements when it is finally executed. - $type = 'Basic'; - - $property = $column; - - // When the column is an id we need to treat it as a graph db id and transform it - // into the form of id(n) and the typecast the value into int. - if ($column == 'id') { - $column = 'id('.$this->modelAsNode().')'; - $value = intval($value); - } - // When it's been already passed in the form of NodeLabel.id we'll have to - // re-format it into id(NodeLabel) - elseif (preg_match('/^.*\.id$/', $column)) { - $parts = explode('.', $column); - $column = sprintf('%s(%s)', $parts[1], $parts[0]); - $value = intval($value); - } - // Also if the $column is already a form of id(n) we'd have to type-cast the value into int. - elseif (preg_match('/^id\(.*\)$/', $column)) { - $value = intval($value); - } - - $binding = $this->prepareBindingColumn($column); - - $this->wheres[] = compact('type', 'binding', 'column', 'operator', 'value', 'boolean'); - - $property = $this->wrap($binding); - - if (!$value instanceof Expression) { - $this->addBinding([$property => $value], 'where'); - } - + // bindings are taken care of at the grammar level return $this; } - /** - * Add an "or where" clause to the query. - * - * @param string $column - * @param string $operator - * @param mixed $value - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function orWhere($column, $operator = null, $value = null) - { - return $this->where($column, $operator, $value, 'or'); - } - - /** - * Determine if the given operator and value combination is legal. - * - * @param string $operator - * @param mixed $value - * - * @return bool - */ - protected function invalidOperatorAndValue($operator, $value) - { - $isOperator = in_array($operator, $this->operators); - - return $isOperator && $operator != '=' && is_null($value); - } - - /** - * Add a raw where clause to the query. - * - * @param string $sql - * @param array $bindings - * @param string $boolean - * - * @return $this - */ - public function whereRaw($sql, array $bindings = [], $boolean = 'and') + public function insert(array $values): bool { - $type = 'raw'; + parent::insert($values); - $this->wheres[] = compact('type', 'sql', 'boolean'); - - $this->addBinding($bindings, 'where'); - - return $this; + // The result might be a summarized result as the connection insert get id hack requires it. + return true; } - - /** - * Add a raw or where clause to the query. - * - * @param string $sql - * @param array $bindings - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function orWhereRaw($sql, array $bindings = []) - { - return $this->whereRaw($sql, $bindings, 'or'); - } - - /** - * Add a where not between statement to the query. - * - * @param string $column - * @param array $values - * @param string $boolean - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function whereNotBetween($column, array $values, $boolean = 'and') - { - return $this->whereBetween($column, $values, $boolean, true); - } - - /** - * Add an or where not between statement to the query. - * - * @param string $column - * @param array $values - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function orWhereNotBetween($column, array $values) - { - return $this->whereNotBetween($column, $values, 'or'); - } - - /** - * Add a nested where statement to the query. - * - * @param \Closure $callback - * @param string $boolean - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function whereNested(Closure $callback, $boolean = 'and') - { - // To handle nested queries we'll actually create a brand new query instance - // and pass it off to the Closure that we have. The Closure can simply do - // do whatever it wants to a query then we will store it for compiling. - $query = $this->newQuery(); - - $query->from($this->from); - - call_user_func($callback, $query); - - return $this->addNestedWhereQuery($query, $boolean); - } - - /** - * Add another query builder as a nested where to the query builder. - * - * @param \Vinelab\NeoEloquent\Query\Builder|static $query - * @param string $boolean - * - * @return $this - */ - public function addNestedWhereQuery($query, $boolean = 'and') - { - if (count($query->wheres)) { - $type = 'Nested'; - - $this->wheres[] = compact('type', 'query', 'boolean'); - - // Now that all the nested queries are been compiled, - // we need to propagate the matches to the parent model. - $this->matches = $query->matches; - - // Set the returned columns. - $this->columns = $query->columns; - - // Set to carry the required nodes and relations - $this->with = $query->with; - - $this->addBinding($query->getBindings(), 'where'); - } - - return $this; - } - - /** - * Add an or where between statement to the query. - * - * @param string $column - * @param array $values - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function orWhereBetween($column, array $values) - { - return $this->whereBetween($column, $values, 'or'); - } - - /** - * Add a full sub-select to the query. - * - * @param string $column - * @param string $operator - * @param \Closure $callback - * @param string $boolean - * - * @return $this - */ - protected function whereSub($column, $operator, Closure $callback, $boolean) - { - $type = 'Sub'; - - $query = $this->newQuery(); - - // Once we have the query instance we can simply execute it so it can add all - // of the sub-select's conditions to itself, and then we can cache it off - // in the array of where clauses for the "main" parent query instance. - call_user_func($callback, $query); - - $this->wheres[] = compact('type', 'column', 'operator', 'query', 'boolean'); - - $this->addBinding($query->getBindings(), 'where'); - - return $this; - } - - /** - * Add an exists clause to the query. - * - * @param \Closure $callback - * @param string $boolean - * @param bool $not - * - * @return $this - */ - public function whereExists(Closure $callback, $boolean = 'and', $not = false) - { - $type = $not ? 'NotExists' : 'Exists'; - - $query = $this->newQuery(); - - // Similar to the sub-select clause, we will create a new query instance so - // the developer may cleanly specify the entire exists query and we will - // compile the whole thing in the grammar and insert it into the SQL. - call_user_func($callback, $query); - - $this->wheres[] = compact('type', 'operator', 'query', 'boolean'); - - $this->addBinding($query->getBindings(), 'where'); - - return $this; - } - - /** - * Add an or exists clause to the query. - * - * @param \Closure $callback - * @param bool $not - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function orWhereExists(Closure $callback, $not = false) - { - return $this->whereExists($callback, 'or', $not); - } - - /** - * Add a where not exists clause to the query. - * - * @param \Closure $callback - * @param string $boolean - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function whereNotExists(Closure $callback, $boolean = 'and') - { - return $this->whereExists($callback, $boolean, true); - } - - /** - * Add a where not exists clause to the query. - * - * @param \Closure $callback - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function orWhereNotExists(Closure $callback) - { - return $this->orWhereExists($callback, true); - } - - /** - * Add an "or where in" clause to the query. - * - * @param string $column - * @param mixed $values - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function orWhereIn($column, $values) - { - return $this->whereIn($column, $values, 'or'); - } - - /** - * Add a "where not in" clause to the query. - * - * @param string $column - * @param mixed $values - * @param string $boolean - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function whereNotIn($column, $values, $boolean = 'and') - { - return $this->whereIn($column, $values, $boolean, true); - } - - /** - * Add an "or where not in" clause to the query. - * - * @param string $column - * @param mixed $values - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function orWhereNotIn($column, $values) - { - return $this->whereNotIn($column, $values, 'or'); - } - - /** - * Add a where in with a sub-select to the query. - * - * @param string $column - * @param \Closure $callback - * @param string $boolean - * @param bool $not - * - * @return $this - */ - protected function whereInSub($column, Closure $callback, $boolean, $not) - { - $type = $not ? 'NotInSub' : 'InSub'; - - // To create the exists sub-select, we will actually create a query and call the - // provided callback with the query so the developer may set any of the query - // conditions they want for the in clause, then we'll put it in this array. - call_user_func($callback, $query = $this->newQuery()); - - $this->wheres[] = compact('type', 'column', 'query', 'boolean'); - - $this->addBinding($query->getBindings(), 'where'); - - return $this; - } - - /** - * Add an "or where null" clause to the query. - * - * @param string $column - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function orWhereNull($column) - { - return $this->whereNull($column, 'or'); - } - - /** - * Add a "where not null" clause to the query. - * - * @param string $column - * @param string $boolean - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function whereNotNull($column, $boolean = 'and') - { - return $this->whereNull($column, $boolean, true); - } - - /** - * Add an "or where not null" clause to the query. - * - * @param string $column - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function orWhereNotNull($column) - { - return $this->whereNotNull($column, 'or'); - } - - /** - * Increment the value of an existing column on a where clause. - * Used to allow querying on the same attribute with different values. - * - * @param string $column - * - * @return string - */ - protected function prepareBindingColumn($column) - { - $count = $this->columnCountForWhereClause($column); - - $binding = ($count > 0) ? $column.'_'.($count + 1) : $column; - - $prefix = $this->from; - if (is_array($prefix)) { - $prefix = implode('_', $prefix); - } - - // we prefix when we do have a prefix ($this->from) and when the column isn't an id (id(abc..)). - $prefix = (!preg_match('/id([a-zA-Z0-9]?)/', $column) && !empty($this->from)) ? mb_strtolower($prefix) : ''; - - return $prefix.$binding; - } - - /** - * Add a "where date" statement to the query. - * - * @param string $column - * @param string $operator - * @param int $value - * @param string $boolean - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function whereDate($column, $operator, $value, $boolean = 'and') - { - return $this->addDateBasedWhere('Date', $column, $operator, $value, $boolean); - } - - /** - * Add a "where day" statement to the query. - * - * @param string $column - * @param string $operator - * @param int $value - * @param string $boolean - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function whereDay($column, $operator, $value, $boolean = 'and') - { - return $this->addDateBasedWhere('Day', $column, $operator, $value, $boolean); - } - - /** - * Add a "where month" statement to the query. - * - * @param string $column - * @param string $operator - * @param int $value - * @param string $boolean - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function whereMonth($column, $operator, $value, $boolean = 'and') - { - return $this->addDateBasedWhere('Month', $column, $operator, $value, $boolean); - } - - /** - * Add a "where year" statement to the query. - * - * @param string $column - * @param string $operator - * @param int $value - * @param string $boolean - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function whereYear($column, $operator, $value, $boolean = 'and') - { - return $this->addDateBasedWhere('Year', $column, $operator, $value, $boolean); - } - - /** - * Add a date based (year, month, day) statement to the query. - * - * @param string $type - * @param string $column - * @param string $operator - * @param int $value - * @param string $boolean - * - * @return $this - */ - protected function addDateBasedWhere($type, $column, $operator, $value, $boolean = 'and') - { - $this->wheres[] = compact('column', 'type', 'boolean', 'operator', 'value'); - - $this->addBinding($value, 'where'); - - return $this; - } - - /** - * Handles dynamic "where" clauses to the query. - * - * @param string $method - * @param string $parameters - * - * @return $this - */ - public function dynamicWhere($method, $parameters) - { - $finder = substr($method, 5); - - $segments = preg_split('/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE); - - // The connector variable will determine which connector will be used for the - // query condition. We will change it as we come across new boolean values - // in the dynamic method strings, which could contain a number of these. - $connector = 'and'; - - $index = 0; - - foreach ($segments as $segment) { - // If the segment is not a boolean connector, we can assume it is a column's name - // and we will add it to the query as a new constraint as a where clause, then - // we can keep iterating through the dynamic method string's segments again. - if ($segment != 'And' && $segment != 'Or') { - $this->addDynamic($segment, $connector, $parameters, $index); - - ++$index; - } - - // Otherwise, we will store the connector so we know how the next where clause we - // find in the query should be connected to the previous ones, meaning we will - // have the proper boolean connector to connect the next where clause found. - else { - $connector = $segment; - } - } - - return $this; - } - - /** - * Add a single dynamic where clause statement to the query. - * - * @param string $segment - * @param string $connector - * @param array $parameters - * @param int $index - */ - protected function addDynamic($segment, $connector, $parameters, $index) - { - // Once we have parsed out the columns and formatted the boolean operators we - // are ready to add it to this query as a where clause just like any other - // clause on the query. Then we'll increment the parameter index values. - $bool = strtolower($connector); - - $this->where(Str::snake($segment), '=', $parameters[$index], $bool); - } - - /** - * Add a "group by" clause to the query. - * - * @param array|string $column,... - * - * @return $this - */ - public function groupBy() - { - foreach (func_get_args() as $arg) { - $this->groups = array_merge((array) $this->groups, is_array($arg) ? $arg : [$arg]); - } - - return $this; - } - - /** - * Add a "having" clause to the query. - * - * @param string $column - * @param string $operator - * @param string $value - * @param string $boolean - * - * @return $this - */ - public function having($column, $operator = null, $value = null, $boolean = 'and') - { - $type = 'basic'; - - $this->havings[] = compact('type', 'column', 'operator', 'value', 'boolean'); - - if (!$value instanceof Expression) { - $this->addBinding($value, 'having'); - } - - return $this; - } - - /** - * Add a "or having" clause to the query. - * - * @param string $column - * @param string $operator - * @param string $value - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function orHaving($column, $operator = null, $value = null) - { - return $this->having($column, $operator, $value, 'or'); - } - - /** - * Add a raw having clause to the query. - * - * @param string $sql - * @param array $bindings - * @param string $boolean - * - * @return $this - */ - public function havingRaw($sql, array $bindings = [], $boolean = 'and') - { - $type = 'raw'; - - $this->havings[] = compact('type', 'sql', 'boolean'); - - $this->addBinding($bindings, 'having'); - - return $this; - } - - /** - * Add a raw or having clause to the query. - * - * @param string $sql - * @param array $bindings - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function orHavingRaw($sql, array $bindings = []) - { - return $this->havingRaw($sql, $bindings, 'or'); - } - - /** - * Add an "order by" clause to the query. - * - * @param string $column - * @param string $direction - * - * @return $this - */ - public function orderBy($column, $direction = 'asc') - { - $property = $this->unions ? 'unionOrders' : 'orders'; - $direction = strtolower($direction) == 'asc' ? 'asc' : 'desc'; - - $this->{$property}[] = compact('column', 'direction'); - - return $this; - } - - /** - * Add an "order by" clause for a timestamp to the query. - * - * @param string $column - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function latest($column = 'created_at') - { - return $this->orderBy($column, 'desc'); - } - - /** - * Add an "order by" clause for a timestamp to the query. - * - * @param string $column - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function oldest($column = 'created_at') - { - return $this->orderBy($column, 'asc'); - } - - /** - * Add a raw "order by" clause to the query. - * - * @param string $sql - * @param array $bindings - * - * @return $this - */ - public function orderByRaw($sql, $bindings = []) - { - $property = $this->unions ? 'unionOrders' : 'orders'; - - $type = 'raw'; - - $this->{$property}[] = compact('type', 'sql'); - - $this->addBinding($bindings, 'order'); - - return $this; - } - - /** - * Set the "offset" value of the query. - * - * @param int $value - * - * @return $this - */ - public function offset($value) - { - $property = $this->unions ? 'unionOffset' : 'offset'; - - $this->$property = max(0, $value); - - return $this; - } - - /** - * Alias to set the "offset" value of the query. - * - * @param int $value - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function skip($value) - { - return $this->offset($value); - } - - /** - * Set the "limit" value of the query. - * - * @param int $value - * - * @return $this - */ - public function limit($value) - { - $property = $this->unions ? 'unionLimit' : 'limit'; - - if ($value > 0) { - $this->$property = $value; - } - - return $this; - } - - /** - * Alias to set the "limit" value of the query. - * - * @param int $value - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function take($value) - { - return $this->limit($value); - } - - /** - * Set the limit and offset for a given page. - * - * @param int $page - * @param int $perPage - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function forPage($page, $perPage = 15) - { - return $this->skip(($page - 1) * $perPage)->take($perPage); - } - - /** - * Add a union statement to the query. - * - * @param \Vinelab\NeoEloquent\Query\Builder|\Closure $query - * @param bool $all - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function union($query, $all = false) - { - if ($query instanceof Closure) { - call_user_func($query, $query = $this->newQuery()); - } - - $this->unions[] = compact('query', 'all'); - - $this->addBinding($query->bindings, 'union'); - - return $this; - } - - /** - * Add a union all statement to the query. - * - * @param \Vinelab\NeoEloquent\Query\Builder|\Closure $query - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function unionAll($query) - { - return $this->union($query, true); - } - - /** - * Lock the selected rows in the table. - * - * @param bool $value - * - * @return $this - */ - public function lock($value = true) - { - $this->lock = $value; - - return $this; - } - - /** - * Lock the selected rows in the table for updating. - * - * @return \Vinelab\NeoEloquent\Query\Builder - */ - public function lockForUpdate() - { - return $this->lock(true); - } - - /** - * Share lock the selected rows in the table. - * - * @return \Vinelab\NeoEloquent\Query\Builder - */ - public function sharedLock() - { - return $this->lock(false); - } - - /** - * Execute a query for a single record by ID. - * - * @param int $id - * @param array $columns - * - * @return mixed|static - */ - public function find($id, $columns = ['*']) - { - return $this->where('id', '=', $id)->first($columns); - } - - /** - * Get a single column's value from the first result of a query. - * - * @param string $column - * - * @return mixed - */ - public function value($column) - { - $result = (array) $this->first([$column]); - - return count($result) > 0 ? reset($result) : null; - } - - /** - * Get a single column's value from the first result of a query. - * - * This is an alias for the "value" method. - * - * @param string $column - * - * @return mixed - * - * @deprecated since version 5.1. - */ - public function pluck($column) - { - return $this->value($column); - } - - /** - * Execute the query and get the first result. - * - * @param array $columns - * - * @return mixed|static - */ - public function first($columns = ['*']) - { - $results = $this->take(1)->get($columns); - - return count($results) > 0 ? reset($results) : null; - } - - /** - * Execute the query as a "select" statement. - * - * @param array $columns - * - * @return array|static[] - */ - public function get($columns = ['*']) - { - return $this->getFresh($columns); - } - - /** - * Paginate the given query into a simple paginator. - * - * @param int $perPage - * @param array $columns - * @param string $pageName - * @param int|null $page - * - * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator - */ - public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null) - { - $page = $page ?: Paginator::resolveCurrentPage($pageName); - - $total = $this->getCountForPagination($columns); - - $results = $this->forPage($page, $perPage)->get($columns); - - return new LengthAwarePaginator($results, $total, $perPage, $page, [ - 'path' => Paginator::resolveCurrentPath(), - 'pageName' => $pageName, - ]); - } - - /** - * Get a paginator only supporting simple next and previous links. - * - * This is more efficient on larger data-sets, etc. - * - * @param int $perPage - * @param array $columns - * @param string $pageName - * - * @return \Illuminate\Contracts\Pagination\Paginator - */ - public function simplePaginate($perPage = 15, $columns = ['*'], $pageName = 'page') - { - $page = Paginator::resolveCurrentPage($pageName); - - $this->skip(($page - 1) * $perPage)->take($perPage + 1); - - return new Paginator($this->get($columns), $perPage, $page, [ - 'path' => Paginator::resolveCurrentPath(), - 'pageName' => $pageName, - ]); - } - - /** - * Get the count of the total records for the paginator. - * - * @param array $columns - * - * @return int - */ - public function getCountForPagination($columns = ['*']) - { - $this->backupFieldsForCount(); - - $this->aggregate = ['function' => 'count', 'columns' => $columns]; - - $results = $this->get(); - - $this->aggregate = null; - - $this->restoreFieldsForCount(); - - if (isset($this->groups)) { - return count($results); - } - - return isset($results[0]) ? (int) array_change_key_case((array) $results[0])['aggregate'] : 0; - } - - /** - * Backup some fields for the pagination count. - */ - protected function backupFieldsForCount() - { - foreach (['orders', 'limit', 'offset', 'columns'] as $field) { - $this->backups[$field] = $this->{$field}; - - $this->{$field} = null; - } - - foreach (['order', 'select'] as $key) { - $this->bindingBackups[$key] = $this->bindings[$key]; - - $this->bindings[$key] = []; - } - } - - /** - * Restore some fields after the pagination count. - */ - protected function restoreFieldsForCount() - { - foreach (['orders', 'limit', 'offset', 'columns'] as $field) { - $this->{$field} = $this->backups[$field]; - } - - foreach (['order', 'select'] as $key) { - $this->bindings[$key] = $this->bindingBackups[$key]; - } - - $this->backups = []; - $this->bindingBackups = []; - } - - /** - * Chunk the results of the query. - * - * @param int $count - * @param callable $callback - */ - public function chunk($count, callable $callback) - { - $results = $this->forPage($page = 1, $count)->get(); - - while (count($results) > 0) { - // On each chunk result set, we will pass them to the callback and then let the - // developer take care of everything within the callback, which allows us to - // keep the memory low for spinning through large result sets for working. - if (call_user_func($callback, $results) === false) { - break; - } - - ++$page; - - $results = $this->forPage($page, $count)->get(); - } - } - - /** - * Get an array with the values of a given column. - * - * @param string $column - * @param string $key - * - * @return array - */ - public function lists($column, $key = null) - { - $columns = $this->getListSelect($column, $key); - - $results = new Collection($this->get($columns)); - - return $results->pluck($columns[0], Arr::get($columns, 1))->all(); - } - - /** - * Get the columns that should be used in a list array. - * - * @param string $column - * @param string $key - * - * @return array - */ - protected function getListSelect($column, $key) - { - $select = is_null($key) ? [$column] : [$column, $key]; - - // If the selected column contains a "dot", we will remove it so that the list - // operation can run normally. Specifying the table is not needed, since we - // really want the names of the columns as it is in this resulting array. - return array_map(function ($column) { - $dot = strpos($column, '.'); - - return $dot === false ? $column : substr($column, $dot + 1); - }, $select); - } - - /** - * Concatenate values of a given column as a string. - * - * @param string $column - * @param string $glue - * - * @return string - */ - public function implode($column, $glue = null) - { - if (is_null($glue)) { - return implode($this->lists($column)); - } - - return implode($glue, $this->lists($column)); - } - - /** - * Determine if any rows exist for the current query. - * - * @return bool - */ - public function exists() - { - $limit = $this->limit; - - $result = $this->limit(1)->count() > 0; - - $this->limit($limit); - - return $result; - } - - /** - * Retrieve the "count" result of the query. - * - * @param string $columns - * - * @return int - */ - public function count($columns = '*') - { - if (!is_array($columns)) { - $columns = [$columns]; - } - - return (int) $this->aggregate(__FUNCTION__, $columns); - } - - /** - * Retrieve the minimum value of a given column. - * - * @param string $column - * - * @return float|int - */ - public function min($column) - { - return $this->aggregate(__FUNCTION__, [$column]); - } - - /** - * Retrieve the maximum value of a given column. - * - * @param string $column - * - * @return float|int - */ - public function max($column) - { - return $this->aggregate(__FUNCTION__, [$column]); - } - - /** - * Retrieve the sum of the values of a given column. - * - * @param string $column - * - * @return float|int - */ - public function sum($column) - { - $result = $this->aggregate(__FUNCTION__, [$column]); - - return $result ?: 0; - } - - /** - * Retrieve the average of the values of a given column. - * - * @param string $column - * - * @return float|int - */ - public function avg($column) - { - return $this->aggregate(__FUNCTION__, [$column]); - } - - /** - * Increment a column's value by a given amount. - * - * @param string $column - * @param int $amount - * @param array $extra - * - * @return int - */ - public function increment($column, $amount = 1, array $extra = []) - { - $wrapped = $this->grammar->wrap($column); - - $columns = array_merge([$column => $this->raw("$wrapped + $amount")], $extra); - - return $this->update($columns); - } - - /** - * Decrement a column's value by a given amount. - * - * @param string $column - * @param int $amount - * @param array $extra - * - * @return int - */ - public function decrement($column, $amount = 1, array $extra = []) - { - $wrapped = $this->grammar->wrap($column); - - $columns = array_merge([$column => $this->raw("$wrapped - $amount")], $extra); - - return $this->update($columns); - } - - /** - * Delete a record from the database. - * - * @param mixed $id - * - * @return int - */ - public function delete($id = null) - { - // If an ID is passed to the method, we will set the where clause to check - // the ID to allow developers to simply and quickly remove a single row - // from their database without manually specifying the where clauses. - if (!is_null($id)) { - $this->where('id', '=', $id); - } - - $cypher = $this->grammar->compileDelete($this); - - $result = $this->connection->delete($cypher, $this->getBindings()); - - if ($result instanceof Result) { - $result = true; - } - - return $result; - } - - /** - * Run a truncate statement on the table. - */ - public function truncate() - { - foreach ($this->grammar->compileTruncate($this) as $sql => $bindings) { - $this->connection->statement($sql, $bindings); - } - } - - /** - * Remove all of the expressions from a list of bindings. - * - * @param array $bindings - * - * @return array - */ - protected function cleanBindings(array $bindings) - { - return array_values(array_filter($bindings, function ($binding) { - return !$binding instanceof Expression; - })); - } - - /** - * Create a raw database expression. - * - * @param mixed $value - * - * @return \Vinelab\NeoEloquent\Query\Expression - */ - public function raw($value) - { - return $this->connection->raw($value); - } - - /** - * Get the raw array of bindings. - * - * @return array - */ - public function getRawBindings() - { - return $this->bindings; - } - - /** - * Set the bindings on the query builder. - * - * @param array $bindings - * @param string $type - * - * @return $this - * - * @throws \InvalidArgumentException - */ - public function setBindings(array $bindings, $type = 'where') - { - if (!array_key_exists($type, $this->bindings)) { - throw new InvalidArgumentException("Invalid binding type: {$type}."); - } - - $this->bindings[$type] = $bindings; - - return $this; - } - - /** - * Merge an array of bindings into our bindings. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * - * @return $this - */ - public function mergeBindings(Builder $query) - { - $this->bindings = array_merge_recursive($this->bindings, $query->bindings); - - return $this; - } - - /** - * Get the database connection instance. - * - * @return \Illuminate\Database\ConnectionInterface - */ - public function getConnection() - { - return $this->connection; - } - - /** - * Get the database query processor instance. - * - * @return \Vinelab\NeoEloquent\Query\Processors\Processor - */ - public function getProcessor() - { - return $this->processor; - } - - /** - * Get the query grammar instance. - * - * @return \Vinelab\NeoEloquent\Query\Grammars\Grammar - */ - public function getGrammar() - { - return $this->grammar; - } - - /** - * Get the number of occurrences of a column in where clauses. - * - * @param string $column - * - * @return int - */ - protected function columnCountForWhereClause($column) - { - if (is_array($this->wheres)) { - return count(array_filter($this->wheres, function ($where) use ($column) { - return $where['column'] == $column; - })); - } - } - - /** - * Add a "where in" clause to the query. - * - * @param string $column - * @param mixed $values - * @param string $boolean - * @param bool $not - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function whereIn($column, $values, $boolean = 'and', $not = false) - { - $type = $not ? 'NotIn' : 'In'; - - // If the value of the where in clause is actually a Closure, we will assume that - // the developer is using a full sub-select for this "in" statement, and will - // execute those Closures, then we can re-construct the entire sub-selects. - if ($values instanceof Closure) { - return $this->whereInSub($column, $values, $boolean, $not); - } - - if ($values instanceof Arrayable) { - $values = $values->toArray(); - } - - $property = $column; - - if ($column == 'id') { - $column = 'id('.$this->modelAsNode().')'; - } - - $this->wheres[] = compact('type', 'column', 'values', 'boolean'); - - $property = $this->wrap($property); - - $this->addBinding([$property => $values], 'where'); - - return $this; - } - - /** - * Add a where between statement to the query. - * - * @param string $column - * @param array $values - * @param string $boolean - * @param bool $not - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function whereBetween($column, array $values, $boolean = 'and', $not = false) - { - $type = 'between'; - - $property = $column; - - if ($column == 'id') { - $column = 'id('.$this->modelAsNode().')'; - } - - $this->wheres[] = compact('column', 'type', 'boolean', 'not'); - - $this->addBinding([$property => $values], 'where'); - - return $this; - } - - /** - * Add a "where null" clause to the query. - * - * @param string $column - * @param string $boolean - * @param bool $not - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function whereNull($column, $boolean = 'and', $not = false) - { - $type = $not ? 'NotNull' : 'Null'; - - if ($column == 'id') { - $column = 'id('.$this->modelAsNode().')'; - } - - $binding = $this->prepareBindingColumn($column); - - $this->wheres[] = compact('type', 'column', 'boolean', 'binding'); - - return $this; - } - - /** - * Add a WHERE statement with carried identifier to the query. - * - * @param string $column - * @param string $operator - * @param string $value - * @param string $boolean - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function whereCarried($column, $operator = null, $value = null, $boolean = 'and') - { - $type = 'Carried'; - - $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean'); - - return $this; - } - - /** - * Add a WITH clause to the query. - * - * @param array $parts - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function with(array $parts) - { - if($this->isAssocArray($parts)) { - foreach ($parts as $key => $part) { - if (!in_array($part, $this->with)) { - $this->with[$key] = $part; - } - } - } else { - foreach ($parts as $part) { - if (!in_array($part, $this->with)) { - $this->with[] = $part; - } - } - } - - return $this; - } - - /** - * Insert a new record into the database. - * - * @param array $values - * - * @return bool - */ - public function insert(array $values) - { - // Since every insert gets treated like a batch insert, we will make sure the - // bindings are structured in a way that is convenient for building these - // inserts statements by verifying the elements are actually an array. - if (!is_array(reset($values))) { - $values = array($values); - } - - // Since every insert gets treated like a batch insert, we will make sure the - // bindings are structured in a way that is convenient for building these - // inserts statements by verifying the elements are actually an array. - else { - foreach ($values as $key => $value) { - $value = $this->formatValue($value); - ksort($value); - $values[$key] = $value; - } - } - - // We'll treat every insert like a batch insert so we can easily insert each - // of the records into the database consistently. This will make it much - // easier on the grammars to just handle one type of record insertion. - $bindings = array(); - - foreach ($values as $record) { - $bindings[] = $record; - } - - $cypher = $this->grammar->compileInsert($this, $values); - - // Once we have compiled the insert statement's Cypher we can execute it on the - // connection and return a result as a boolean success indicator as that - // is the same type of result returned by the raw connection instance. - $bindings = $this->cleanBindings($bindings); - - $results = $this->connection->insert($cypher, $bindings); - - return !!$results; - } - - /** - * Create a new node with related nodes with one database hit. - * - * @param array $model - * @param array $related - * - * @return \Vinelab\NeoEloquent\Eloquent\Model - */ - public function createWith(array $model, array $related) - { - $cypher = $this->grammar->compileCreateWith($this, compact('model', 'related')); - - // Indicate that we need the result returned as is. - return $this->connection->statement($cypher, [], true); - } - - /** - * Execute the query as a fresh "select" statement. - * - * @param array $columns - * - * @return array|static[] - */ - public function getFresh($columns = array('*')) - { - if (is_null($this->columns)) { - $this->columns = $columns; - } - - return $this->runSelect(); - } - - /** - * Run the query as a "select" statement against the connection. - * - * @return array - */ - protected function runSelect() - { - return $this->connection->select($this->toCypher(), $this->getBindings()); - } - - /** - * Get the Cypher representation of the traversal. - * - * @return string - */ - public function toCypher() - { - return $this->grammar->compileSelect($this); - } - - /** - * Add a relationship MATCH clause to the query. - * - * @param \Vinelab\NeoEloquent\Eloquent\Model $parent The parent model of the relationship - * @param \Vinelab\NeoEloquent\Eloquent\Model $related The related model - * @param string $relatedNode The related node' placeholder - * @param string $relationship The relationship title - * @param string $property The parent's property we are matching against - * @param string $value - * @param string $direction Possible values are in, out and in-out - * @param string $boolean And, or operators - * - * @return \Vinelab\NeoEloquent\Query\Builder|static - */ - public function matchRelation($parent, $related, $relatedNode, $relationship, $property, $value = null, $direction = 'out', $boolean = 'and') - { - $parentLabels = $parent->nodeLabel(); - $relatedLabels = $related->nodeLabel(); - $parentNode = $this->modelAsNode($parentLabels); - - $this->matches[] = array( - 'type' => 'Relation', - 'optional' => $boolean, - 'property' => $property, - 'direction' => $direction, - 'relationship' => $relationship, - 'parent' => array( - 'node' => $parentNode, - 'labels' => $parentLabels, - ), - 'related' => array( - 'node' => $relatedNode, - 'labels' => $relatedLabels, - ), - ); - - $this->addBinding(array($this->wrap($property) => $value), 'matches'); - - return $this; - } - - public function matchMorphRelation($parent, $relatedNode, $property, $value = null, $direction = 'out', $boolean = 'and') - { - $parentLabels = $parent->nodeLabel(); - $parentNode = $this->modelAsNode($parentLabels); - - $this->matches[] = array( - 'type' => 'MorphTo', - 'optional' => 'and', - 'property' => $property, - 'direction' => $direction, - 'related' => array('node' => $relatedNode), - 'parent' => array( - 'node' => $parentNode, - 'labels' => $parentLabels, - ), - ); - - $this->addBinding(array($property => $value), 'matches'); - - return $this; - } - - /** - * the percentile of a given value over a group, - * with a percentile from 0.0 to 1.0. - * It uses a rounding method, returning the nearest value to the percentile. - * - * @param string $column - * - * @return mixed - */ - public function percentileDisc($column, $percentile = 0.0) - { - return $this->aggregate(__FUNCTION__, array($column), $percentile); - } - - /** - * Retrieve the percentile of a given value over a group, - * with a percentile from 0.0 to 1.0. It uses a linear interpolation method, - * calculating a weighted average between two values, - * if the desired percentile lies between them. - * - * @param string $column - * - * @return mixed - */ - public function percentileCont($column, $percentile = 0.0) - { - return $this->aggregate(__FUNCTION__, array($column), $percentile); - } - - /** - * Retrieve the standard deviation for a given column. - * - * @param string $column - * - * @return mixed - */ - public function stdev($column) - { - return $this->aggregate(__FUNCTION__, array($column)); - } - - /** - * Retrieve the standard deviation of an entire group for a given column. - * - * @param string $column - * - * @return mixed - */ - public function stdevp($column) - { - return $this->aggregate(__FUNCTION__, array($column)); - } - - /** - * Get the collected values of the give column. - * - * @param string $column - * - * @return \Illuminate\Database\Eloquent\Collection - */ - public function collect($column) - { - $row = $this->aggregate(__FUNCTION__, array($column)); - - $collected = []; - - foreach ($row as $value) { - $collected[] = $value; - } - - return new Collection($collected); - } - - /** - * Get the count of the disctinct values of a given column. - * - * @param string $column - * - * @return int - */ - public function countDistinct($column) - { - return (int) $this->aggregate(__FUNCTION__, array($column)); - } - - /** - * Execute an aggregate function on the database. - * - * @param string $function - * @param array $columns - * - * @return mixed - */ - public function aggregate($function, $columns = array('*'), $percentile = null) - { - $this->aggregate = array_merge([ - 'label' => $this->from, - ], compact('function', 'columns', 'percentile')); - - $previousColumns = $this->columns; - - $results = $this->get($columns); - - // Once we have executed the query, we will reset the aggregate property so - // that more select queries can be executed against the database without - // the aggregate value getting in the way when the grammar builds it. - $this->aggregate = null; - - $this->columns = $previousColumns; - - $values = $this->getRecordsByPlaceholders($results); - - $value = reset($values); - if(is_array($value)) { - return current($value); - } else { - return $value; - } - } - - /** - * Add a binding to the query. - * - * @param mixed $value - * @param string $type - * - * @return \Vinelab\NeoEloquent\Query\Builder - */ - public function addBinding($value, $type = 'where') - { - if (is_array($value)) { - $key = array_keys($value)[0]; - - if (strpos($key, '.') !== false) { - $binding = $value[$key]; - unset($value[$key]); - $key = explode('.', $key)[1]; - $value[$key] = $binding; - } - } - - if (!array_key_exists($type, $this->bindings)) { - throw new \InvalidArgumentException("Invalid binding type: {$type}."); - } - - if (is_array($value)) { - $this->bindings[$type] = array_merge($this->bindings[$type], $value); - } else { - $this->bindings[$type][] = $value; - } - - return $this; - } - - /** - * Convert a string into a Neo4j Label. - * - * @param string $label - * - * @return Everyman\Neo4j\Label - */ - public function makeLabel($label) - { - return $this->client->makeLabel($label); - } - - /** - * Tranfrom a model's name into a placeholder - * for fetched properties. i.e.:. - * - * MATCH (user:`User`)... "user" is what this method returns - * out of User (and other labels). - * PS: It consideres the first value in $labels - * - * @param array $labels - * - * @return string - */ - public function modelAsNode(array $labels = null) - { - $labels = (!is_null($labels)) ? $labels : $this->from; - - return $this->grammar->modelAsNode($labels); - } - - /** - * Merge an array of where clauses and bindings. - * - * @param array $wheres - * @param array $bindings - */ - public function mergeWheres($wheres, $bindings) - { - $this->wheres = array_merge((array) $this->wheres, (array) $wheres); - - $this->bindings['where'] = array_merge_recursive($this->bindings['where'], (array) $bindings); - } - - public function wrap($property) - { - return $this->grammar->getIdReplacement($property); - } - - /** - * Get a new instance of the query builder. - * - * @return \Vinelab\NeoEloquent\Query\Builder - */ - public function newQuery() - { - return new self($this->connection, $this->grammar); - } - - /** - * Fromat the value into its string representation. - * - * @param mixed $value - * - * @return string - */ - protected function formatValue($value) - { - // If the value is a date we'll format it according to the specified - // date format. - if ($value instanceof DateTime || $value instanceof Carbon) { - $value = $value->format($this->grammar->getDateFormat()); - } - - return $value; - } - - /* - * Add/Drop labels - * @param $labels array array of strings(labels) - * @param $operation string 'add' or 'drop' - * @return bool true if success, otherwise false - */ - public function updateLabels($labels, $operation = 'add') - { - $cypher = $this->grammar->compileUpdateLabels($this, $labels, $operation); - - $result = $this->connection->update($cypher, $this->getBindings()); - - return (bool) $result; - } - - public function getNodesCount($result) - { - return count($this->getNodeRecords($result)); - } - - /** - * Handle dynamic method calls into the method. - * - * @param string $method - * @param array $parameters - * - * @return mixed - * - * @throws \BadMethodCallException - */ - public function __call($method, $parameters) - { - if (Str::startsWith($method, 'where')) { - return $this->dynamicWhere($method, $parameters); - } - - $className = get_class($this); - - throw new BadMethodCallException("Call to undefined method {$className}::{$method}()"); - } - - /** - * Determine whether an array is associative. - * - * @param array $array - * - * @return bool - */ - protected function isAssocArray($array) - { - return is_array($array) && array_keys($array) !== range(0, count($array) - 1); - } - } diff --git a/src/Query/Contracts/IlluminateToQueryStructureDecorator.php b/src/Query/Contracts/IlluminateToQueryStructureDecorator.php new file mode 100644 index 00000000..f22ef11c --- /dev/null +++ b/src/Query/Contracts/IlluminateToQueryStructureDecorator.php @@ -0,0 +1,11 @@ +media)); + } + + public function context(): array + { + return [ + 'media' => $this->media, + ]; + } +} diff --git a/src/Query/Exceptions/NonWriteableRelationshipException.php b/src/Query/Exceptions/NonWriteableRelationshipException.php new file mode 100644 index 00000000..ff983334 --- /dev/null +++ b/src/Query/Exceptions/NonWriteableRelationshipException.php @@ -0,0 +1,22 @@ + $this->type, + 'direction' => $this->direction, + ]; + } +} diff --git a/src/Query/Expression.php b/src/Query/Expression.php deleted file mode 100644 index 70e90e88..00000000 --- a/src/Query/Expression.php +++ /dev/null @@ -1,44 +0,0 @@ -value = $value; - } - - /** - * Get the value of the expression. - * - * @return mixed - */ - public function getValue() - { - return $this->value; - } - - /** - * Get the value of the expression. - * - * @return string - */ - public function __toString() - { - return (string) $this->getValue(); - } -} diff --git a/src/Query/Grammars/CypherGrammar.php b/src/Query/Grammars/CypherGrammar.php deleted file mode 100644 index e14e81e6..00000000 --- a/src/Query/Grammars/CypherGrammar.php +++ /dev/null @@ -1,1169 +0,0 @@ -columns)) { - $query->columns = array('*'); - } - - return trim($this->concatenate($this->compileComponents($query))); - } - - /** - * Compile the components necessary for a select clause. - * - * @param \Vinelab\NeoEloquent\Query\Builder - * @param array|string $specified You may specify a component to compile - * - * @return array - */ - protected function compileComponents(Builder $query, $specified = null) - { - $cypher = array(); - - $components = array(); - - // Setup the components that we need to compile - if ($specified) { - // We support passing a string as well - // by turning it into an array as needed - // to be $components - if (!is_array($specified)) { - $specified = array($specified); - } - - $components = $specified; - } else { - $components = $this->selectComponents; - } - - foreach ($components as $component) { - // Compiling return for Neo4j is - // handled in the compileColumns method - // in order to keep the convenience provided by Eloquent - // that deals with collecting and processing the columns - if ($component == 'return') { - $component = 'columns'; - } - - $cypher[$component] = $this->compileComponent($query, $components, $component); - } - - return $cypher; - } - - /** - * Compile a single component. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $components - * @param string $component - * - * @return string - */ - protected function compileComponent(Builder $query, $components, $component) - { - $cypher = ''; - - // Let's make sure this is a proprietary component that we support - if (!in_array($component, $components)) { - throw new InvalidCypherGrammarComponentException($component); - } - - // To compile the query, we'll spin through each component of the query and - // see if that component exists. If it does we'll just call the compiler - // function for the component which is responsible for making the Cypher. - if (!is_null($query->$component)) { - $method = 'compile'.ucfirst($component); - - $cypher = $this->$method($query, $query->$component); - } - - return $cypher; - } - - /** - * Compile the MATCH for a query with relationships. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $matches - * - * @return string - */ - public function compileMatches(Builder $query, $matches) - { - if (!is_array($matches) || empty($matches)) { - // when no matches are specified fallback to using the 'from' key - $component = $this->compileComponents($query, ['from']); - $cypher = $component['from']; - } else { - $optionalMatches = []; - $mandatoryMatches = []; - foreach ($matches as $match) { - - switch($match['optional']) { - case 'or': - $optionalMatches[] = $match; - - break; - - case 'and': - $mandatoryMatches[] = $match; - - break; - } - } - - $cypher = $this->compileMandatoryMatchesCypher($query, $mandatoryMatches); - - $cypher = $cypher.' '.$this->compileOptionalMatchesCypher($optionalMatches); - } - - return $cypher; - } - - public function compileMandatoryMatchesCypher($query, $matches) - { - $prepared = []; - foreach ($matches as $match) { - $method = 'prepareMatch'.ucfirst($match['type']); - $prepared[] = $this->$method($match); - } - - // If no mandatory matches are available force match the base model. - return !empty($prepared) ? 'MATCH '.implode(', ', $prepared) : $this->compileFrom($query, $query->from, true); - } - - public function compileOptionalMatchesCypher($matches) - { - $optional = ''; - foreach ($matches as $match) { - $method = 'prepareMatch'.ucfirst($match['type']); - $optional = $optional.' OPTIONAL MATCH '.$this->$method($match); - } - - return isset($optional) ? $optional : ''; - } - - /** - * Prepare a query for MATCH using - * collected $matches of type Relation. - * - * @param array $match - * - * @return string - */ - public function prepareMatchRelation(array $match) - { - $parent = $match['parent']; - $related = $match['related']; - $property = $match['property']; - $direction = $match['direction']; - $relationship = $match['relationship']; - $parentNode = $parent['node']; - $relatedNode = $related['node']; - - // Prepare labels for query. - $parentLabels = $this->prepareLabels($parent['labels']); - $relatedLabels = $this->prepareLabels($related['labels']); - - // Get the relationship ready for query - $relationshipLabel = $this->prepareRelation($relationship, $relatedNode); - - // We treat node ids differently here in Cypher - // so we will have to turn it into something like id(node) - $property = $property == 'id' ? 'id('.$parentNode.')' : $parentNode.'.'.$property; - - return '('.$parentNode.$parentLabels.'), ' - .$this->craftRelation($parentNode, $relationshipLabel, $relatedNode, $relatedLabels, $direction); - } - - /** - * Prepare a query for MATCH using - * collected $matches of Type MorphTo. - * - * @param array $match - * - * @return string - */ - public function prepareMatchMorphTo(array $match) - { - $parent = $match['parent']; - $related = $match['related']; - $property = $match['property']; - $direction = $match['direction']; - - // Prepare labels and node for query - $relatedNode = $related['node']; - $parentLabels = $this->prepareLabels($parent['labels']); - - // We treat node ids differently here in Cypher - // so we will have to turn it into something like id(node) - $property = $property == 'id' ? 'id('.$parent['node'].')' : $parent['node'].'.'.$property; - - return '('.$parent['node'].$parentLabels.'), ' - .$this->craftRelation($parent['node'], 'r', $relatedNode, '', $direction); - } - - /** - * Craft a Cypher relationship of any type: - * INCOMING, OUTGOING or BIDIRECTIONAL. - * - * examples: - * --------- - * OUTGOING - * [user:User]-[:POSTED]->[post:Post] - * - * INCOMING - * [phone:Phone]<-[:PHONE]-[owner:User] - * - * BIDIRECTIONAL - * [user:User]<-(:FOLLOWS)->[follower:User] - * - * @param string $parentNode The parent Model's node placeholder - * @param string $relationLabel The label of the relationship i.e. :PHONE - * @param string $relatedNode The related Model's node placeholder - * @param string $relatedLabels Labels of of related Node - * @param string $direction Where is it going? - * - * @return string - */ - public function craftRelation($parentNode, $relationLabel, $relatedNode, $relatedLabels, $direction, $bare = false) - { - switch (strtolower($direction)) { - case 'out': - $relation = '(%s)-[%s]->%s'; - break; - - case 'in': - $relation = '(%s)<-[%s]-%s'; - break; - - default: - $relation = '(%s)-[%s]-%s'; - break; - } - - return ($bare) ? sprintf($relation, $parentNode, $relationLabel, $relatedNode) - : sprintf($relation, $parentNode, $relationLabel, '('.$relatedNode.$relatedLabels.')'); - } - - /** - * Compile the "from" portion of the query - * which in cypher represents the nodes we're MATCHing. - * The forceMatch flag, forces the "from" model to be matched and thus returned in the query. - * This is required in cases where all matches are optional, leading to an invalid syntax where - * a query starts with an `OPTIONAL MATCH`. This flag would force a `MATCH` to preced it. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * @param string $labels - * @param bool $forceMatch - * - * @return string - */ - public function compileFrom(Builder $query, $labels, $forceMatch = false) - { - if(!$forceMatch) { - // Only compile when no relational matches are specified, - // mostly used for simple queries. - if (!empty($query->matches)) { - return ''; - } - } - $labels = $this->prepareLabels($labels); - - // every label must begin with a ':' so we need to check - // and reformat if need be. - $labels = ':'.preg_replace('/^:/', '', $labels); - - // now we add the default placeholder for this node - $labels = $query->modelAsNode().$labels; - - return sprintf('MATCH (%s)', $labels); - } - - /** - * Compile the "where" portions of the query. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * - * @return string - */ - protected function compileWheres(Builder $query) - { - $cypher = array(); - - if (is_null($query->wheres)) { - return ''; - } - - // Each type of where clauses has its own compiler function which is responsible - // for actually creating the where clauses Cypher. This helps keep the code nice - // and maintainable since each clause has a very small method that it uses. - foreach ($query->wheres as $where) { - $method = "where{$where['type']}"; - - $cypher[] = $where['boolean'].' '.$this->$method($query, $where); - } - - // If we actually have some where clauses, we will strip off the first boolean - // operator, which is added by the query builders for convenience so we can - // avoid checking for the first clauses in each of the compilers methods. - if (count($cypher) > 0) { - $cypher = implode(' ', $cypher); - - return 'WHERE '.preg_replace('/and |or /', '', $cypher, 1); - } - - return ''; - } - - /** - * Compile a basic where clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * - * @return string - */ - protected function whereBasic(Builder $query, $where) - { - $value = $this->parameter($where); - - return $this->wrap($where['column']).' '.$where['operator'].' '.$value; - } - - /** - * Compiled a WHERE clause with carried identifiers. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * @param array $where - * - * @return string - */ - protected function whereCarried(Builder $query, $where) - { - return $where['column'].' '.$where['operator'].' '.$where['value']; - } - - /** - * Compile the "limit" portions of the query. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * @param int $limit - * - * @return string - */ - protected function compileLimit(Builder $query, $limit) - { - return 'LIMIT '.(int) $limit; - } - - /** - * Compile the "SKIP" portions of the query. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * @param int $offset - * - * @return string - */ - protected function compileOffset(Builder $query, $offset) - { - return 'SKIP '.(int) $offset; - } - - /** - * Compile the "RETURN *" portion of the query. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * @param array $columns - * - * @return string - */ - protected function compileColumns(Builder $query, $properties) - { - // When we have an aggregate we will have to return it instead of the plain columns - // since aggregates for Cypher are not calculated at the beginning of the query like Cypher - // instead we'll have to return in a form such as: RETURN max(user.logins). - if (!is_null($query->aggregate)) { - return $this->compileAggregate($query, $query->aggregate); - } - - $node = $this->query->modelAsNode(); - - // We need to make sure that there exists relations so that we return - // them as well, also there has to be nothing carried in the query - // to not conflict with them. - if ($this->hasMatchRelations($query) && empty($query->with)) { - $relations = $this->getMatchRelations($query); - $identifiers = []; - - foreach ($relations as $relation) { - $identifiers[] = $this->getRelationIdentifier($relation['relationship'], $relation['related']['node']); - } - - $properties = array_merge($properties, $identifiers); - } - - // In the case where the query has relationships - // we need to return the requested properties as is - // since they are considered node placeholders. - if (!empty($query->matches)) { - $columns = implode(', ', array_values($properties)); - } else { - $columns = $this->columnize($properties); - // when asking for specific properties (not *) we add - // the node placeholder so that we can get the nodes and - // the relationships themselves returned - if (!in_array('*', $properties) && !in_array($node, $properties)) { - $columns .= ", $node"; - } - } - - $distinct = ($query->distinct) ? 'DISTINCT ' : ''; - - return 'RETURN '.$distinct.$columns; - } - - /** - * Compile the "order by" portions of the query. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * @param array $orders - * - * @return string - */ - public function compileOrders(Builder $query, $orders) - { - return 'ORDER BY '.implode(', ', array_map(function ($order) { - return $this->wrap($order['column']).' '.mb_strtoupper($order['direction']); - }, $orders)); - } - - /** - * Compile a create statement into Cypher. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * @param array $values - * - * @return string - */ - public function compileCreate(Builder $query, $values) - { - $labels = $this->prepareLabels($query->from); - - $columns = $this->columnsFromValues($values); - - $node = $query->modelAsNode(); - - return "CREATE ({$node}{$labels}) SET {$columns} RETURN {$node}"; - } - - /** - * Compile an update statement into Cypher. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * @param array $values - * - * @return string - */ - public function compileUpdate(Builder $query, $values) - { - $columns = $this->columnsFromValues($values, true); - - // Of course, update queries may also be constrained by where clauses so we'll - // need to compile the where clauses and attach it to the query so only the - // intended records are updated by the Cypher statements we generate to run. - $where = $this->compileWheres($query); - - // We always need the MATCH clause in our Cypher which - // is the responsibility of compiling the From component. - $match = $this->compileComponents($query, array('from')); - $match = $match['from']; - - // When updating we need to return the count of the affected nodes - // so we trick the Columns compiler into returning that for us. - $return = $this->compileColumns($query, array('count('.$query->modelAsNode().')')); - - return "$match $where SET $columns $return"; - } - - public function postfixValues(array $values, $updating = false) - { - $postfix = $updating ? '_update' : '_create'; - - $processed = []; - - foreach ($values as $key => $value) { - $processed[$key.$postfix] = $value; - } - - return $processed; - } - - public function columnsFromValues(array $values, $updating = false) - { - $columns = []; - // Each one of the columns in the update statements needs to be wrapped in the - // keyword identifiers, also a place-holder needs to be created for each of - // the values in the list of bindings so we can make the sets statements. - - foreach ($values as $key => $value) { - // Update bindings are differentiated with an _update postfix to make sure the don't clash - // with query bindings. - $postfix = $updating ? '_update' : '_create'; - - $columns[] = $this->wrap($key).' = '.$this->parameter(array('column' => $key.$postfix)); - } - - return implode(', ', $columns); - } - - /** - * Compile a "where in" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * - * @return string - */ - protected function whereIn(Builder $query, $where) - { - $values = $this->valufy($where['values']); - - return $this->wrap($where['column']).' IN '.$values; - } - - /** - * Compile a "where not in" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * - * @return string - */ - protected function whereNotIn(Builder $query, $where) - { - $values = $this->valufy($where['values']); - - return 'NOT '.$this->wrap($where['column']).' IN '.$values; - } - - /** - * Compile a nested where clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * - * @return string - */ - protected function whereNested(Builder $query, $where) - { - $nested = $where['query']; - - return '('.substr($this->compileWheres($nested), 6).')'; - } - - /** - * Compile a where condition with a sub-select. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * - * @return string - */ - protected function whereSub(Builder $query, $where) - { - $select = $this->compileSelect($where['query']); - - return $this->wrap($where['column']).' '.$where['operator']." ($select)"; - } - - /** - * Compile a "where null" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * - * @return string - */ - protected function whereNull(Builder $query, $where) - { - return $this->wrap($where['column']).' is null'; - } - - /** - * Compile a "where not null" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * - * @return string - */ - protected function whereNotNull(Builder $query, $where) - { - return $this->wrap($where['column']).' is not null'; - } - - /** - * Compile a "where date" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * - * @return string - */ - protected function whereDate(Builder $query, $where) - { - return $this->dateBasedWhere('date', $query, $where); - } - - /** - * Compile a "where day" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * - * @return string - */ - protected function whereDay(Builder $query, $where) - { - return $this->dateBasedWhere('day', $query, $where); - } - - /** - * Compile a "where month" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * - * @return string - */ - protected function whereMonth(Builder $query, $where) - { - return $this->dateBasedWhere('month', $query, $where); - } - - /** - * Compile a "where year" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * - * @return string - */ - protected function whereYear(Builder $query, $where) - { - return $this->dateBasedWhere('year', $query, $where); - } - - /** - * Compile a date based where clause. - * - * @param string $type - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * - * @return string - */ - protected function dateBasedWhere($type, Builder $query, $where) - { - $value = $this->parameter($where['value']); - - return $type.'('.$this->wrap($where['column']).') '.$where['operator'].' '.$value; - } - - /** - * Compile the "having" portions of the query. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $havings - * - * @return string - */ - protected function compileHavings(Builder $query, $havings) - { - $cypher = implode(' ', array_map([$this, 'compileHaving'], $havings)); - - return 'with '.$this->removeLeadingBoolean($cypher); - } - - /** - * Compile a single having clause. - * - * @param array $having - * - * @return string - */ - protected function compileHaving(array $having) - { - // If the having clause is "raw", we can just return the clause straight away - // without doing any more processing on it. Otherwise, we will compile the - // clause into Cypher based on the components that make it up from builder. - if ($having['type'] === 'raw') { - return $having['boolean'].' '.$having['cypher']; - } - - return $this->compileBasicHaving($having); - } - - /** - * Compile a basic having clause. - * - * @param array $having - * - * @return string - */ - protected function compileBasicHaving($having) - { - $column = $this->wrap($having['column']); - - $parameter = $this->parameter($having['value']); - - return $having['boolean'].' '.$column.' '.$having['operator'].' '.$parameter; - } - - /** - * Compile a delete statement into Cypher. - * - * @param \Illuminate\Database\Query\Builder $query - * - * @return string - */ - public function compileDelete(Builder $query, $isRelationship = false, $shouldKeepEndNode = false) - { - // We always need the MATCH clause in our Cypher which - // is the responsibility of compiling the From component. - $matchComponent = $this->compileComponents($query, array('matches')); - $matchCypher = $matchComponent['matches']; - - $where = is_array($query->wheres) ? $this->compileWheres($query) : ''; - - // by default we assume that we're deleting the start node - // so we set the identifier accordingly (the placeholder of the startn node) - $returnIdentifiers = $query->modelAsNode(); - - // now we determine whether we're deleting a relationship, - // in this case the identifier that we're targeting is - // then the identifier of the relationship and the end node. - if ($isRelationship) { - // when deleting the relationship we should not delete - // the start node, only the relationship and optionally - // the end node so we will clear whatever identifier we had. - $returnIdentifiers = ''; - foreach ($query->matches as $match) { - // determine whether we should also delete the end node - if (!$shouldKeepEndNode) { - $returnIdentifiers .= $match['related']['node'].', '; - } - - $returnIdentifiers .= $this->getRelationIdentifier($match['relationship'], $match['related']['node']); - } - - $matchCypher .= $where; - } else { - - // when deleting the start node must not have any relations left - // so when asked to delete the start node we'll add an - // OPTIONAL MATCH (n)-[r]-() where n is the node - // we're matching in this query. - $matchCypher .= $where.' OPTIONAL MATCH ('.$query->modelAsNode().')-[r]-()'; - $returnIdentifiers .= ', r'; - } - - return "$matchCypher DELETE $returnIdentifiers"; - } - - public function compileWith(Builder $query, $with) - { - $parts = []; - - if (!empty($with)) { - foreach ($with as $identifier => $part) { - $parts[] = (!is_numeric($identifier)) ? "$identifier AS $part" : $part; - } - - return 'WITH '.implode(', ', $parts); - } - } - - /** - * Compile an insert statement into Cypher. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * - * @return string - */ - public function compileInsert(Builder $query, array $values) - { - /* - * Essentially we will force every insert to be treated as a batch insert which - * simply makes creating the Cypher easier for us since we can utilize the same - * basic routine regardless of an amount of records given to us to insert. - * - * We are working on getting a Cypher like this: - * CREATE (:Wiz {fiz: 'foo', biz: 'boo'}). (:Wiz {fiz: 'morefoo', biz: 'moreboo'}) - */ - - if (!is_array($query->from)) { - $query->from = array($query->from); - } - - $label = $this->prepareLabels($query->from); - - if (!is_array(reset($values))) { - $values = array($values); - } - - // Prepare the values to be sent into the entities factory as - // ['label' => ':Wiz', 'bindings' => ['fiz' => 'foo', 'biz' => 'boo']] - $values = array_map(function ($entity) use ($label) { - return ['label' => $label, 'bindings' => $entity]; - }, $values); - // We need to build a list of parameter place-holders of values that are bound to the query. - return 'CREATE '.$this->prepareEntities($values); - } - - public function compileMatchRelationship(Builder $query, $attributes) - { - $startKey = $attributes['start']['id']['key']; - $startNode = $this->modelAsNode($attributes['start']['label']); - $startLabel = $this->prepareLabels($attributes['start']['label']); - - if ($startKey === 'id') { - $startKey = 'id('.$startNode.')'; - $startId = (int) $attributes['start']['id']['value']; - } else { - $startKey = $startNode.'.'.$startKey; - $startId = '"'.addslashes($attributes['start']['id']['value']).'"'; - } - - $startCondition = $startKey.'='.$startId; - - $query = "MATCH ($startNode$startLabel)"; - - // we account for no-end relationships. - if (isset($attributes['end'])) { - $endKey = $attributes['end']['id']['key']; - $endNode = 'rel_'.$this->modelAsNode($attributes['label']); - $endLabel = $this->prepareLabels($attributes['end']['label']); - - if ($attributes['end']['id']['value']) { - if ($endKey === 'id') { - // when it's 'id' it has to be numeric - $endKey = 'id('.$endNode.')'; - $endId = (int) $attributes['end']['id']['value']; - } else { - $endKey = $endNode.'.'.$endKey; - $endId = '"'.addslashes($attributes['end']['id']['value']).'"'; - } - } - - $endCondition = (!empty($endId)) ? $endKey.'='.$endId : ''; - - $query .= ", ($endNode$endLabel)"; - } - - $query .= " WHERE $startCondition"; - - if (!empty($endCondition)) { - $query .= " AND $endCondition"; - } - - return $query; - } - - /** - * Compile a query that creates a relationship between two given nodes. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * @param array $attributes - * - * @return string - */ - public function compileRelationship(Builder $query, $attributes, $addEndLabel = false) - { - $startNode = $this->modelAsNode($attributes['start']['label']); - $endNode = 'rel_'.$this->modelAsNode($attributes['label']); - - // support crafting relationships for unknown end nodes, - // i.e. fetching the relationships of a certain type - // for a given start node. - $endLabel = 'r'; - if (isset($attributes['end'])) { - $endLabel = $this->prepareLabels($attributes['end']['label']); - if ($addEndLabel) { - $endNode .= $endLabel; - } - } - - $query = $this->craftRelation( - $startNode, - 'r:'.$attributes['label'], - '('.$endNode.')', - $endLabel, - $attributes['direction'], - true - ); - - $properties = $attributes['properties']; - - if (!empty($properties)) { - foreach ($properties as $key => $value) { - unset($properties[$key]); - // we do not accept IDs for relations - if ($key === 'id') { - continue; - } - $properties[] = 'r.'.$key.' = '.$this->valufy($value); - } - - $query .= ' SET '.implode(', ', $properties); - } - - return $query; - } - - public function compileCreateRelationship(Builder $query, $attributes) - { - $match = $this->compileMatchRelationship($query, $attributes); - $relationQuery = $this->compileRelationship($query, $attributes); - $query = "$match MERGE $relationQuery"; - $startIdentifier = $this->modelAsNode($attributes['start']['label']); - $endIdentifier = 'rel_'.$this->modelAsNode($attributes['label']); - $query .= " RETURN r,$startIdentifier,$endIdentifier"; - - return $query; - } - - public function compileDeleteRelationship(Builder $query, $attributes) - { - $match = $this->compileMatchRelationship($query, $attributes); - $relation = $this->compileRelationship($query, $attributes); - $query = "$match MATCH $relation DELETE r"; - - return $query; - } - - public function compileGetRelationship(Builder $builder, $attributes) - { - $match = $this->compileMatchRelationship($builder, $attributes); - $relation = $this->compileRelationship($builder, $attributes, true); - $startIdentifier = $this->modelAsNode($attributes['start']['label']); - $endIdentifier = 'rel_'.$this->modelAsNode($attributes['label']); - $query = "$match MATCH $relation RETURN r,$startIdentifier,$endIdentifier"; - - return $query; - } - - /** - * Compile a query that creates multiple nodes of multiple model types related all together. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * @param array $create - * - * @return string - */ - public function compileCreateWith(Builder $query, $create) - { - $model = $create['model']; - $related = $create['related']; - $identifier = true; // indicates that we this entity requires an identifier for prepareEntity. - - // Prepare the parent model as a query entity with an identifier to be - // later used when relating with the rest of the models, something like: - // (post:`Post` {title: '..', body: '...'}) - $entity = $this->prepareEntity([ - 'label' => $model['label'], - 'bindings' => $model['attributes'], - ], $identifier); - - $parentNode = $this->modelAsNode($model['label']); - - // Prepare the related models as entities for the query. - $relations = []; - $attachments = []; - $createdIdsToReturn = []; - $attachedIdsToReturn = []; - - foreach ($related as $with) { - $idKey = $with['id']; - $label = $with['label']; - $values = $with['create']; - $attach = $with['attach']; - $relation = $with['relation']; - - if (!is_array($values)) { - $values = (array) $values; - } - - // Indicate a bare new relation when being crafted so that we distinguish it from relations - // b/w existing records. - $bare = true; - - // We need to craft a relationship between the parent model's node identifier - // and every single relationship record so that we get something like this: - // (post)-[:PHOTO]->(:Photo {url: '', caption: '..'}) - foreach ($values as $bindings) { - $identifier = $this->getUniqueLabel($relation['name']); - // return this identifier as part of the result. - $createdIdsToReturn[] = $identifier; - // get a relation cypher. - $relations[] = $this->craftRelation( - $parentNode, - ':'.$relation['type'], - $this->prepareEntity(compact('label', 'bindings'), $identifier), - $this->modelAsNode($label), - $relation['direction'], - $bare - ); - } - - // Set up the query parts that are required to attach two nodes. - if (!empty($attach)) { - $identifier = $this->getUniqueLabel($relation['name']); - // return this identifier as part of the result. - $attachedIdsToReturn[] = $identifier; - // Now we deal with our attachments so that we create the conditional - // queries for each relation that we need to attach. - // $node = $this->modelAsNode($label, $relation['name']); - $nodeLabel = $this->prepareLabels($label); - - // An attachment query is a combination of MATCH, WHERE and CREATE where - // we MATCH the nodes that we need to attach, set the conditions - // on the records that we need to attach with WHERE and then - // CREATE these relationships. - $attachments['matches'][] = "({$identifier}{$nodeLabel})"; - - if ($idKey === 'id') { - // Native Neo4j IDs are treated differently - $attachments['wheres'][] = "id($identifier) IN [".implode(', ', $attach).']'; - } else { - $attachments['wheres'][] = "$identifier.$idKey IN [\"".implode('", "', $attach).'"]'; - } - - $attachments['relations'][] = $this->craftRelation( - $parentNode, - ':'.$relation['type'], - "($identifier)", - $nodeLabel, - $relation['direction'], - $bare - ); - } - } - // Return the Cypher representation of the query that would look something like: - // CREATE (post:`Post` {title: '..', body: '..'}) - $cypher = 'CREATE '.$entity; - // Then we add the records that we need to create as such: - // (post)-[:PHOTO]->(:`Photo` {url: ''}), (post)-[:VIDEO]->(:`Video` {title: '...'}) - if (!empty($relations)) { - $cypher .= ', '.implode(', ', $relations); - } - // Now we add the attaching Cypher - if (!empty($attachments)) { - // Bring the parent node along with us to be used in the query further. - $cypher .= " WITH $parentNode"; - - if (!empty($createdIdsToReturn)) { - $cypher .= ', '.implode(', ', $createdIdsToReturn); - } - - // MATCH the related nodes that we are attaching. - $cypher .= ' MATCH '.implode(', ', $attachments['matches']); - // Set the WHERE conditions for the heart of the query. - $cypher .= ' WHERE '.implode(' AND ', $attachments['wheres']); - // CREATE the relationships between matched nodes - $cypher .= ' MERGE'.implode(', ', $attachments['relations']); - } - - $cypher .= " RETURN $parentNode, ".implode(', ', array_merge($createdIdsToReturn, $attachedIdsToReturn)); - - return $cypher; - } - - public function compileAggregate(Builder $query, $aggregate) - { - $distinct = null; - $function = $aggregate['function']; - // When calling for the distinct count we'll set the distinct flag and ask for the count function. - if ($function == 'countDistinct') { - $function = 'count'; - $distinct = 'DISTINCT '; - } - - $node = $this->modelAsNode($query->from); - - // We need to format the columns to be in the form of n.property unless it is a *. - $columns = implode(', ', array_map(function ($column) use ($node) { - return $column == '*' ? $column : "$node.$column"; - }, $aggregate['columns'])); - - if (isset($aggregate['percentile']) && !is_null($aggregate['percentile'])) { - $percentile = $aggregate['percentile']; - - return "RETURN $function($columns, $percentile)"; - } - - return "RETURN $function($distinct$columns)"; - } - - /** - * Compile an statement to add or drop node labels. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * @param array $labels labels as string like :label1:label2 etc - * @param array $operation type of operation 'add' or 'drop' - * - * @return string - */ - public function compileUpdateLabels(Builder $query, $labels, $operation = 'add') - { - if (trim(strtolower($operation)) == 'add') { - $updateType = 'SET'; - } else { - $updateType = 'REMOVE'; - } - // Each one of the columns in the update statements needs to be wrapped in the - // keyword identifiers, also a place-holder needs to be created for each of - // the values in the list of bindings so we can make the sets statements. - - $labels = $query->modelAsNode().$this->prepareLabels($labels); - - // Of course, update queries may also be constrained by where clauses so we'll - // need to compile the where clauses and attach it to the query so only the - // intended records are updated by the Cypher statements we generate to run. - $where = $this->compileWheres($query); - - // We always need the MATCH clause in our Cypher which - // is the responsibility of compiling the From component. - $match = $this->compileComponents($query, array('from')); - $match = $match['from']; - - return "$match $where $updateType $labels RETURN ".$query->modelAsNode(); - } -} diff --git a/src/Query/Grammars/Grammar.php b/src/Query/Grammars/Grammar.php deleted file mode 100644 index 1b5ba166..00000000 --- a/src/Query/Grammars/Grammar.php +++ /dev/null @@ -1,492 +0,0 @@ -getValue(); - } - - /** - * Get the appropriate query parameter place-holder for a value. - * - * @param mixed $value - * - * @return string - */ - public function parameter($value) - { - - // Validate whether the requested field is the - // node id, in that case id(n) doesn't work as - // a placeholder so we transform it to the id replacement instead. - - // When coming from a WHERE statement we'll have to pluck out the column - // from the collected attributes. - if (is_array($value) && isset($value['binding'])) { - $value = $value['binding']; - } elseif (is_array($value) && isset($value['column'])) { - $value = $value['column']; - } elseif ($this->isExpression($value)) { - $value = $this->getValue($value); - } - - $property = $this->getIdReplacement($value); - - if (strpos($property, '.') !== false) { - $property = explode('.', $property)[1]; - } - - return '$'.$property; - } - - /** - * Prepare a label by formatting it as expected, - * trim out trailing spaces and add backticks. - * - * @var string - * - * @return string - */ - public function prepareLabels($labels) - { - if (is_array($labels)) { - // get the labels prepared and back to a string imploded by : they go. - $labels = implode('', array_map(array($this, 'wrapLabel'), $labels)); - } - - return $labels; - } - - /** - * Make sure the label is wrapped with backticks. - * - * @param string $label - * - * @return string - */ - public function wrapLabel($label) - { - // every label must begin with a ':' so we need to check - // and reformat if need be. - return trim(':`'.preg_replace('/^:/', '', $label).'`'); - } - - /** - * Prepare a relationship label. - * - * @param string $relation - * @param string $related - * - * @return string - */ - public function prepareRelation($relation, $related) - { - return $this->getRelationIdentifier($relation, $related).":{$relation}"; - } - - /** - * Get the identifier for the given relationship. - * - * @param string $relation - * @param string $related - * - * @return string - */ - public function getRelationIdentifier($relation, $related) - { - return 'rel_'.mb_strtolower($relation).'_'.$related; - } - - /** - * Turn labels like this ':User:Admin' - * into this 'user_admin'. - * - * @param string $labels - * - * @return string - */ - public function normalizeLabels($labels) - { - return mb_strtolower(str_replace(':', '_', preg_replace('/^:/', '', $labels))); - } - - /** - * Wrap a value in keyword identifiers. - * - * @param string $value - * - * @return string - */ - public function wrap($value, $prefixAlias = false) - { - // We will only wrap the value unless it has parentheses - // in it which is the case where we're matching a node by id, or an * - // and last whether this is a pre-formatted key. - if (preg_match('/[(|)]/', $value) || $value == '*' || strpos($value, '.') !== false) { - return $value; - } - - // In the case where the developer specifies the properties and not returning - // everything, we need to check whether the primaryKey is meant to be returned - // since Neo4j's way of evaluating returned properties for the Node id is - // different: id(n) instead of n.id - - if ($value == 'id') { - return 'id('.$this->query->modelAsNode().')'; - } - - return $this->query->modelAsNode().'.'.$value; - } - - /** - * Wrap a single string in keyword identifiers. - * - * @param string $value - * - * @return string - */ - protected function wrapValue($value) - { - if ($value === '*') { - return $value; - } - - return '"'.str_replace('"', '""', $value).'"'; - } - - /** - * Wrap an array of values. - * - * @param array $values - * - * @return array - */ - public function wrapArray(array $values) - { - return array_map([$this, 'wrap'], $values); - } - - /** - * Turn an array of values into a comma separated string of values - * that are escaped and ready to be passed as values in a query. - * - * @param array $values - * - * @return string - */ - public function valufy($values) - { - $arrayValue = true; - - // we'll only deal with arrays so let's turn it into one if it isn't - if (!is_array($values)) { - $arrayValue = false; - $values = [$values]; - } - - // escape and wrap them with a quote. - $values = array_map(function ($value) { - // First, we check whether we have a date instance so that - // we take its string representation instead. - if ($value instanceof DateTime || $value instanceof Carbon) { - $value = $value->format($this->getDateFormat()); - } - - // We need to keep the data type of values - // except when they're strings, we need to - // escape wrap them. - if (is_string($value)) { - $value = "'".addslashes($value)."'"; - } - // In order to support boolean value types and not have PHP convert them to their - // corresponding string values, we'll have to handle boolean values and add their literal string representation. - elseif (is_bool($value)) { - $value = ($value) ? 'true' : 'false'; - } - - return $value; - - }, $values); - - // stringify them. - return $arrayValue ? '['.implode(',', $values).']' : implode(', ', $values); - } - - /** - * Get a model's name as a Node placeholder. - * - * i.e. in "MATCH (user:`User`)"... "user" is what this method returns - * - * @param string|array $labels The labels we're choosing from - * @param bool $related Tells whether this is a related node so that we append a 'with_' to label. - * - * @return string - */ - public function modelAsNode($labels = null, $relation = null) - { - if (is_null($labels)) { - return 'n'; - } elseif (is_array($labels)) { - $labels = implode('_', $labels); // Or just replace with this - } - - // When this is a related node we'll just prepend it with 'with_' that way we avoid - // clashing node models in the cases like using recursive model relations. - // @see https://github.com/Vinelab/NeoEloquent/issues/7 - if (!is_null($relation)) { - $labels = 'with_'.$relation.'_'.$labels; - } - - return mb_strtolower($labels); - } - - /** - * Set the query builder for this grammar instance. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - */ - public function setQuery($query) - { - $this->query = $query; - } - - /** - * Get the replacement of an id property. - * - * @return string - */ - public function getIdReplacement($column) - { - // If we have id(n) we're removing () and keeping idn - $column = preg_replace('/[(|)]/', '', $column); - // Check whether the column is still id so that we transform it to the form id(n) and then - // recursively calling ourself to reformat accordingly. - if ($column == 'id') { - $from = (!is_null($this->query)) ? $this->query->from : null; - $column = $this->getIdReplacement('id('.$this->modelAsNode($from).')'); - } - // When it's a form of node.attribute we'll just remove the '.' so that - // we get a consistent form of binding key/value pairs. - elseif (strpos($column, '.')) { - return str_replace('.', '', $column); - } - - return $column; - } - - /** - * Prepare properties and values to be injected in a query. - * - * @param array $values - * - * @return string - */ - public function prepareEntities(array $entities) - { - return implode(', ', array_map([$this, 'prepareEntity'], $entities)); - } - - /** - * Prepare an entity's values to be used in a query, performs sanitization and reformatting. - * - * @param array $entity - * - * @return string - */ - public function prepareEntity($entity, $identifier = false) - { - $label = (is_array($entity['label'])) ? $this->prepareLabels($entity['label']) : $entity['label']; - - if ($identifier) { - // when the $identifier is used as a flag, we'll take care of generating it. - if ($identifier === true) { - $identifier = $this->modelAsNode($entity['label']); - } - - $label = $identifier.$label; - } - - $bindings = $entity['bindings']; - - $properties = []; - foreach ($bindings as $key => $value) { - // From the Neo4j docs: - // "NULL is not a valid property value. NULLs can instead be modeled by the absence of a key." - // So we'll just ignore null keys if they occur. - if (is_null($value)) { - continue; - } - - $key = $this->propertize($key); - $value = $this->valufy($value); - $properties[] = "$key: $value"; - } - - return "($label { ".implode(', ', $properties).'})'; - } - - /** - * Concatenate an array of segments, removing empties. - * - * @param array $segments - * @return string - */ - protected function concatenate($segments) - { - return implode(' ', array_filter($segments, function ($value) { - return (string) $value !== ''; - })); - } - - /** - * Turn a string into a valid property for a query. - * - * @param string $property - * - * @return string - */ - public function propertize($property) - { - // Sanitize the string from all characters except alpha numeric. - return preg_replace('/[^A-Za-z0-9._,-:]+/i', '', $property); - } - - /** - * Get the unique identifier for the given label. - * - * @param array $label The normalized label(s) - * @param int $number Will be appended for uniqueness (must be handled on the client side) - * - * @return string - */ - public function getLabelIdentifier(array $label) - { - return $this->getUniqueLabel(reset($label)); - } - - /** - * Get a unique label for the given label. - * - * @param string $label - * - * @return string - */ - public function getUniqueLabel($label) - { - return $label.$this->labelPostfix.uniqid(); - } - - /** - * Crop the postfixed part of the label removes the part that - * gets added by getUniqueLabel. - * - * @param string $id - * - * @return string - */ - public function cropLabelIdentifier($id) - { - return preg_replace('/_neoeloquent_.*/', '', $id); - } - - /** - * Convert an array of column names into a delimited string. - * - * @param array $columns - * - * @return string - */ - public function columnize(array $columns) - { - return implode(', ', array_map([$this, 'wrap'], $columns)); - } - - /** - * Check whether the given query has relation matches. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * - * @return bool - */ - public function hasMatchRelations(Builder $query) - { - return (bool) count($this->getMatchRelations($query)); - } - - /** - * Get the relation-based matches from the given query. - * - * @param \Vinelab\NeoEloquent\Query\Builder $query - * - * @return array - */ - public function getMatchRelations(Builder $query) - { - return array_filter($query->matches, function ($match) { - return $match['type'] == 'Relation'; - }); - } -} diff --git a/src/Query/Processors/Processor.php b/src/Query/Processors/Processor.php deleted file mode 100644 index f0c8bd9e..00000000 --- a/src/Query/Processors/Processor.php +++ /dev/null @@ -1,52 +0,0 @@ -getConnection()->insert($cypher, $values); - - $id = $query->getConnection()->getPdo()->lastInsertId($sequence); - - return is_numeric($id) ? (int) $id : $id; - } - - /** - * Process the results of a column listing query. - * - * @param array $results - * - * @return array - */ - public function processColumnListing($results) - { - return $results; - } -} diff --git a/src/Relations/RelatesTo.php b/src/Relations/RelatesTo.php new file mode 100644 index 00000000..6421c471 --- /dev/null +++ b/src/Relations/RelatesTo.php @@ -0,0 +1,1849 @@ +' + ) { + parent::__construct($queryFromLeft, $right); + } + + public function fromLeftToRight(): self + { + $this->direction = '>'; + + return $this; + } + + public function fromRightToLeft(): self + { + $this->direction = '<'; + + return $this; + } + + public function fromAnyDirection(): self + { + $this->direction = ''; + + return $this; + } + + public function addConstraints(): void + { + $this->whereRelationship($this->relationshipType, $this->getRightLabel(), $this->direction); + } + + public function addEagerConstraints(array $models): void + { + $this->whereIn($this->getQualifiedRightKeyName(), $this->parseIds($models)); + } + + public function initRelation(array $models, $relation): array + { + foreach ($models as $model) { + $model->setRelation($relation, $this->getDefaultFor($model)); + } + + return $models; + } + + /** + * Toggles a model (or models) from the parent. + * + * Each existing model is detached, and non existing ones are attached. + * + * @return array{attached: list, detached: list} + */ + public function toggle(mixed $ids, bool $touch = true): array + { + $changes = []; + + $toToggle = $this->formatRecordsList($this->parseIds($ids)); + $connectedRhs = $this->fetchKeysOfConnectedNodesAtRightHandSide(); + + $changes['detached'] = $this->detachAndReturnTheirIds($connectedRhs, $toToggle); + $changes['attached'] = $this->attachAndReturnTheirIds($connectedRhs, $toToggle); + + $this->touchIfNeeded($touch, $changes); + + return $changes; + } + + /** + * Cast the given key to convert to primary key type. + */ + protected function castKey(mixed $key): mixed + { + return $this->getTypeSwapValue($this->getRightModel()->getKeyType(), $key); + } + + /** + * Converts a given value to a given type value. + */ + protected function getTypeSwapValue(string $type, mixed $value): mixed + { + return match (strtolower($type)) { + 'int', 'integer' => (int) $value, + 'real', 'float', 'double' => (float) $value, + 'string' => (string) $value, + default => $value, + }; + } + + /** + * Sync the intermediate tables with a list of IDs without detaching. + */ + public function syncWithoutDetaching(Model|array|BaseCollection $ids): array + { + return $this->sync($ids, false); + } + + /** + * Sync the intermediate tables with a list of IDs or collection of models. + * + * @return array{attached: array, detached: array, updated: array} + */ + public function sync(BaseCollection|Model|array $ids, bool $detaching = true): array + { + $changes = []; + + $current = $this->fetchKeysOfConnectedNodesAtRightHandSide(); + $toSync = $this->formatRecordsList($this->parseIds($ids)); + + if ($detaching) { + $changes['detached'] = $this->detachAndReturnTheirIds($current, $toSync); + } + + $changes = array_merge($changes, $this->attachNew($toSync, $current, false)); + + if (count(Arr::flatten($changes)) > 0) { + $this->touchIfTouching(); + } + + return $changes; + } + + /** + * Sync the intermediate tables with a list of IDs or collection of models with the given pivot values. + * + * @param BaseCollection|Model|array $ids + */ + public function syncWithPivotValues(mixed $ids, array $values, bool $detaching = true): array + { + return $this->sync(collect($this->parseIds($ids))->mapWithKeys(function ($id) use ($values) { + return [$id => $values]; + }), $detaching); + } + + /** + * Format the sync / toggle record list so that it is keyed by ID. + */ + protected function formatRecordsList(array $records): array + { + return collect($records)->mapWithKeys(function ($attributes, $id) { + if (! is_array($attributes)) { + return [$attributes, []]; + } + + return [$id => $attributes]; + })->all(); + } + + /** + * Attach all of the records that aren't in the given current records. + * + * @return array{attached: array, updated: array} + */ + protected function attachNew(array $records, array $current, bool $touch = true): array + { + $changes = ['attached' => [], 'updated' => []]; + + foreach ($records as $id => $attributes) { + if (! in_array($id, $current)) { + $this->attach($id, $attributes, $touch); + + $changes['attached'][] = $this->castKey($id); + } elseif (count($attributes) > 0 && + $this->updateExistingRelationship($id, $attributes, $touch)) { + $changes['updated'][] = $this->castKey($id); + } + } + + return $changes; + } + + /** + * Update an existing pivot record on the table. + */ + public function updateExistingRelationship(mixed $id, array $attributes, bool $touch = true): int + { + if ($this->using && + empty($this->relationWheres) && + empty($this->relationWhereIns) && + empty($this->relationWhereNulls)) { + return $this->updateExistingPivotUsingCustomClass($id, $attributes, $touch); + } + + if ($this->hasRelationshipColumn($this->updatedAt())) { + $attributes = $this->addTimestampsToAttachment($attributes, true); + } + + $updated = $this->newRelationshipStatementForId($this->parseId($id)) + ->update($this->castAttributes($attributes)); + + if ($touch) { + $this->touchIfTouching(); + } + + return $updated; + } + + /** + * Update an existing pivot record on the table via a custom class. + * + * @param mixed $id + * @param bool $touch + * @return int + */ + protected function updateExistingPivotUsingCustomClass($id, array $attributes, $touch) + { + $pivot = $this->getCurrentlyAttachedPivots() + ->where($this->foreignPivotKey, $this->parent->{$this->parentKey}) + ->where($this->relatedPivotKey, $this->parseId($id)) + ->first(); + + $updated = $pivot ? $pivot->fill($attributes)->isDirty() : false; + + if ($updated) { + $pivot->save(); + } + + if ($touch) { + $this->touchIfTouching(); + } + + return (int) $updated; + } + + /** + * Attach a model to the parent. + */ + public function attach(mixed $id, array $attributes = [], bool $touch = true): void + { + $this->newRelationStatement()->insert($this->formatAttachRecords( + $this->parseIds($id), $attributes + )); + + if ($touch) { + $this->touchIfTouching(); + } + } + + /** + * Get all of the IDs from the given mixed value. + */ + protected function parseIds(mixed $value): array + { + if ($value instanceof Model) { + return [$value->{$this->getParentKeyName()}]; + } + + if ($value instanceof Collection) { + return $value->pluck($this->relatedKey)->all(); + } + + if ($value instanceof BaseCollection) { + return $value->toArray(); + } + + return (array) $value; + } + + /** + * @return list + */ + public function fetchKeysOfConnectedNodesAtRightHandSide(): array + { + return $this->newRelationshipQuery() + ->pluck($this->getQualifiedRightKeyName()) + ->all(); + } + + /** + * @param list $available + * @param array $toDetach + */ + protected function detachAndReturnTheirIds(array $available, array $toDetach): array + { + $detached = []; + $toDetach = array_diff($available, array_keys($toDetach)); + + if (count($toDetach) > 0) { + $this->detach($toDetach); + + $detached = array_map($this->castKey(...), $toDetach); + } + + return $detached; + } + + /** + * @param list $available + * @param array $toAttach + */ + protected function attachAndReturnTheirIds(array $available, array $toAttach): array + { + $attach = array_diff_key($toAttach, array_flip($available)); + $attached = []; + if (count($attach) > 0) { + $this->attach($attach, [], false); + + $attached = array_keys($attach); + } + + return $attached; + } + + public function touchIfNeeded(bool $touch, array $changes): void + { + if ($touch && + count(Arr::flatten($changes)) > 0) { + $this->touchIfTouching(); + } + } + + /** + * Attach a model to the parent using a custom class. + * + * @param mixed $id + * @return void + */ + protected function attachUsingCustomClass($id, array $attributes) + { + $records = $this->formatAttachRecords( + $this->parseIds($id), $attributes + ); + + foreach ($records as $record) { + $this->newRelation($record, false)->save(); + } + } + + /** + * Create an array of records to insert into the pivot table. + * + * @return array + */ + protected function formatAttachRecords(array $ids, array $attributes) + { + $records = []; + + $hasTimestamps = ($this->hasRelationshipColumn($this->createdAt()) || + $this->hasRelationshipColumn($this->updatedAt())); + + // To create the attachment records, we will simply spin through the IDs given + // and create a new record to insert for each ID. Each ID may actually be a + // key in the array, with extra attributes to be placed in other columns. + foreach ($ids as $key => $value) { + $records[] = $this->formatAttachRecord( + $key, $value, $attributes, $hasTimestamps + ); + } + + return $records; + } + + /** + * Create a full attachment record payload. + * + * @param int $key + * @param mixed $value + * @param array $attributes + * @param bool $hasTimestamps + * @return array + */ + protected function formatAttachRecord($key, $value, $attributes, $hasTimestamps) + { + [$id, $attributes] = $this->extractAttachIdAndAttributes($key, $value, $attributes); + + return array_merge( + $this->baseAttachRecord($id, $hasTimestamps), $this->castAttributes($attributes) + ); + } + + /** + * Get the attach record ID and extra attributes. + * + * @param mixed $key + * @param mixed $value + * @return array + */ + protected function extractAttachIdAndAttributes($key, $value, array $attributes) + { + return is_array($value) + ? [$key, array_merge($value, $attributes)] + : [$value, $attributes]; + } + + /** + * Create a new pivot attachment record. + * + * @param int $id + * @param bool $timed + * @return array + */ + protected function baseAttachRecord($id, $timed) + { + $record[$this->relatedPivotKey] = $id; + + $record[$this->foreignPivotKey] = $this->parent->{$this->parentKey}; + + // If the record needs to have creation and update timestamps, we will make + // them by calling the parent model's "freshTimestamp" method which will + // provide us with a fresh timestamp in this model's preferred format. + if ($timed) { + $record = $this->addTimestampsToAttachment($record); + } + + foreach ($this->defaultRelationValues as $value) { + $record[$value['column']] = $value['value']; + } + + return $record; + } + + /** + * Set the creation and update timestamps on an attach record. + */ + protected function addTimestampsToAttachment(array $record, bool $exists = false): array + { + $fresh = $this->parent->freshTimestamp(); + + if ($this->using) { + $relationshipModel = new $this->using; + + $fresh = $fresh->format($relationshipModel->getDateFormat()); + } + + if (! $exists && $this->hasRelationshipColumn($this->createdAt())) { + $record[$this->createdAt()] = $fresh; + } + + if ($this->hasRelationshipColumn($this->updatedAt())) { + $record[$this->updatedAt()] = $fresh; + } + + return $record; + } + + /** + * Determine whether the given column is defined as a pivot column. + */ + public function hasRelationshipColumn(string $column): bool + { + return in_array($column, $this->relationshipColumns); + } + + /** + * Detach models from the relationship. + * + * @param mixed $ids + * @param bool $touch + * @return int + */ + public function detach($ids = null, $touch = true) + { + if ($this->using && + ! empty($ids) && + empty($this->relationWheres) && + empty($this->relationWhereIns) && + empty($this->relationWhereNulls)) { + $results = $this->detachUsingCustomClass($ids); + } else { + $query = $this->newRelationshipQuery(); + + // If associated IDs were passed to the method we will only delete those + // associations, otherwise all of the association ties will be broken. + // We'll return the numbers of affected rows when we do the deletes. + if (! is_null($ids)) { + $ids = $this->parseIds($ids); + + if (empty($ids)) { + return 0; + } + + $query->whereIn($this->getQualifiedRelatedPivotKeyName(), (array) $ids); + } + + // Once we have all of the conditions set on the statement, we are ready + // to run the delete on the pivot table. Then, if the touch parameter + // is true, we will go ahead and touch all related models to sync. + $results = $query->delete(); + } + + if ($touch) { + $this->touchIfTouching(); + } + + return $results; + } + + /** + * Detach models from the relationship using a custom class. + * + * @param mixed $ids + * @return int + */ + protected function detachUsingCustomClass($ids) + { + $results = 0; + + foreach ($this->parseIds($ids) as $id) { + $results += $this->newRelation([ + $this->foreignPivotKey => $this->parent->{$this->parentKey}, + $this->relatedPivotKey => $id, + ], true)->delete(); + } + + return $results; + } + + /** + * Get the pivot models that are currently attached. + * + * @return BaseCollection + */ + protected function getCurrentlyAttachedPivots() + { + return $this->newRelationshipQuery()->get()->map(function ($record) { + $class = $this->using ?: Pivot::class; + + $pivot = $class::fromRawAttributes($this->parent, (array) $record, $this->getTable(), true); + + return $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey); + }); + } + + /** + * Create a new pivot model instance. + * + * @return Model&AsRelationship + */ + public function newRelation(array $attributes = [], bool $exists = false): Model + { + $attributes = array_merge(array_column($this->defaultRelationValues, 'value', 'column'), $attributes); + + $pivot = $this->related->newPivot( + $this->parent, $attributes, $this->table, $exists, $this->using + ); + + return $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey); + } + + /** + * Create a new existing relation model instance. + * + * @return Model&AsRelationship + */ + public function newExistingRelation(array $attributes = []): Model + { + return $this->newRelation($attributes, true); + } + + /** + * Get a new plain query builder for the pivot table. + */ + public function newRelationStatement(): Builder + { + $query = $this->query->getQuery() + ->newQuery(); + + if (! $query instanceof Builder) { + throw new \LogicException(sprintf('Query is an instance of %s, but must be one of %s', $query::class, Builder::class)); + } + + return $query + ->from($this->getLeftLabel()) + ->whereRelationship($this->relationshipType, $this->getRightLabel()); + } + + /** + * Get a new pivot statement for a given "other" ID. + */ + public function newRelationshipStatementForId(mixed $id): Builder + { + return $this->newRelationshipQuery() + ->whereIn($this->getQualifiedRightKeyName(), $this->parseIds($id)); + } + + public function newRelationshipQuery(): Builder + { + $query = $this->newRelationStatement(); + + foreach ($this->relationWheres as $arguments) { + $query->where(...$arguments); + } + + foreach ($this->relationWhereIns as $arguments) { + $query->whereIn(...$arguments); + } + + foreach ($this->relationWhereNulls as $arguments) { + $query->whereNull(...$arguments); + } + + return $query->where($this->getQualifiedForeignPivotKeyName(), $this->parent->{$this->parentKey}); + } + + /** + * Set the columns on the pivot table to retrieve. + * + * @param array|mixed $columns + * @return $this + */ + public function withPivot($columns) + { + $this->relationshipColumns = array_merge( + $this->relationshipColumns, is_array($columns) ? $columns : func_get_args() + ); + + return $this; + } + + /** + * Attempt to resolve the intermediate table name from the given string. + * + * @param string $table + * @return string + */ + protected function resolveTableName($table) + { + if (! str_contains($table, '\\') || ! class_exists($table)) { + return $table; + } + + $model = new $table; + + if (! $model instanceof Model) { + return $table; + } + + if (in_array(AsPivot::class, class_uses_recursive($model))) { + $this->using($table); + } + + return $model->getTable(); + } + + /** + * Set the where clause for the relation query. + * + * @return $this + */ + protected function addWhereConstraints() + { + $this->query->where( + $this->getQualifiedForeignPivotKeyName(), '=', $this->parent->{$this->parentKey} + ); + + return $this; + } + + /** + * Match the eagerly loaded results to their parents. + * + * @param string $relation + * @return array + */ + public function match(array $models, Collection $results, $relation) + { + $dictionary = $this->buildDictionary($results); + + // Once we have an array dictionary of child objects we can easily match the + // children back to their parent using the dictionary and the keys on the + // parent models. Then we should return these hydrated models back out. + foreach ($models as $model) { + $key = $this->getDictionaryKey($model->{$this->parentKey}); + + if (isset($dictionary[$key])) { + $model->setRelation( + $relation, $this->related->newCollection($dictionary[$key]) + ); + } + } + + return $models; + } + + /** + * Build model dictionary keyed by the relation's foreign key. + * + * @return array + */ + protected function buildDictionary(Collection $results) + { + // First we'll build a dictionary of child models keyed by the foreign key + // of the relation so that we will easily and quickly match them to the + // parents without having a possibly slow inner loop for every model. + $dictionary = []; + + foreach ($results as $result) { + $value = $this->getDictionaryKey($result->{$this->accessor}->{$this->foreignPivotKey}); + + $dictionary[$value][] = $result; + } + + return $dictionary; + } + + /** + * Get the class being used for pivot models. + * + * @return string + */ + public function getPivotClass() + { + return $this->using ?? Pivot::class; + } + + /** + * Specify the custom pivot model to use for the relationship. + * + * @param string $class + * @return $this + */ + public function using($class) + { + $this->using = $class; + + return $this; + } + + /** + * Specify the custom pivot accessor to use for the relationship. + * + * @param string $accessor + * @return $this + */ + public function as($accessor) + { + $this->accessor = $accessor; + + return $this; + } + + /** + * Set a where clause for a pivot table column. + * + * @param string $column + * @param mixed $operator + * @param mixed $value + * @param string $boolean + * @return $this + */ + public function wherePivot($column, $operator = null, $value = null, $boolean = 'and') + { + $this->relationWheres[] = func_get_args(); + + return $this->where($this->qualifyRelationshipColumn($column), $operator, $value, $boolean); + } + + /** + * Set a "where between" clause for a pivot table column. + * + * @param string $column + * @param string $boolean + * @param bool $not + * @return $this + */ + public function wherePivotBetween($column, array $values, $boolean = 'and', $not = false) + { + return $this->whereBetween($this->qualifyRelationshipColumn($column), $values, $boolean, $not); + } + + /** + * Set a "or where between" clause for a pivot table column. + * + * @param string $column + * @return $this + */ + public function orWherePivotBetween($column, array $values) + { + return $this->wherePivotBetween($column, $values, 'or'); + } + + /** + * Set a "where pivot not between" clause for a pivot table column. + * + * @param string $column + * @param string $boolean + * @return $this + */ + public function wherePivotNotBetween($column, array $values, $boolean = 'and') + { + return $this->wherePivotBetween($column, $values, $boolean, true); + } + + /** + * Set a "or where not between" clause for a pivot table column. + * + * @param string $column + * @return $this + */ + public function orWherePivotNotBetween($column, array $values) + { + return $this->wherePivotBetween($column, $values, 'or', true); + } + + /** + * Set a "where in" clause for a pivot table column. + * + * @param string $column + * @param mixed $values + * @param string $boolean + * @param bool $not + * @return $this + */ + public function wherePivotIn($column, $values, $boolean = 'and', $not = false) + { + $this->relationWhereIns[] = func_get_args(); + + return $this->whereIn($this->qualifyRelationshipColumn($column), $values, $boolean, $not); + } + + /** + * Set an "or where" clause for a pivot table column. + * + * @param string $column + * @param mixed $operator + * @param mixed $value + * @return $this + */ + public function orWherePivot($column, $operator = null, $value = null) + { + return $this->wherePivot($column, $operator, $value, 'or'); + } + + /** + * Set a where clause for a pivot table column. + * + * In addition, new pivot records will receive this value. + * + * @param string|array $column + * @param mixed $value + * @return $this + * + * @throws InvalidArgumentException + */ + public function withPivotValue($column, $value = null) + { + if (is_array($column)) { + foreach ($column as $name => $value) { + $this->withPivotValue($name, $value); + } + + return $this; + } + + if (is_null($value)) { + throw new InvalidArgumentException('The provided value may not be null.'); + } + + $this->defaultRelationValues[] = compact('column', 'value'); + + return $this->wherePivot($column, '=', $value); + } + + /** + * Set an "or where in" clause for a pivot table column. + * + * @param string $column + * @param mixed $values + * @return $this + */ + public function orWherePivotIn($column, $values) + { + return $this->wherePivotIn($column, $values, 'or'); + } + + /** + * Set a "where not in" clause for a pivot table column. + * + * @param string $column + * @param mixed $values + * @param string $boolean + * @return $this + */ + public function wherePivotNotIn($column, $values, $boolean = 'and') + { + return $this->wherePivotIn($column, $values, $boolean, true); + } + + /** + * Set an "or where not in" clause for a pivot table column. + * + * @param string $column + * @param mixed $values + * @return $this + */ + public function orWherePivotNotIn($column, $values) + { + return $this->wherePivotNotIn($column, $values, 'or'); + } + + /** + * Set a "where null" clause for a pivot table column. + * + * @param string $column + * @param string $boolean + * @param bool $not + * @return $this + */ + public function wherePivotNull($column, $boolean = 'and', $not = false) + { + $this->relationWhereNulls[] = func_get_args(); + + return $this->whereNull($this->qualifyRelationshipColumn($column), $boolean, $not); + } + + /** + * Set a "where not null" clause for a pivot table column. + * + * @param string $column + * @param string $boolean + * @return $this + */ + public function wherePivotNotNull($column, $boolean = 'and') + { + return $this->wherePivotNull($column, $boolean, true); + } + + /** + * Set a "or where null" clause for a pivot table column. + * + * @param string $column + * @param bool $not + * @return $this + */ + public function orWherePivotNull($column, $not = false) + { + return $this->wherePivotNull($column, 'or', $not); + } + + /** + * Set a "or where not null" clause for a pivot table column. + * + * @param string $column + * @return $this + */ + public function orWherePivotNotNull($column) + { + return $this->orWherePivotNull($column, true); + } + + /** + * Add an "order by" clause for a pivot table column. + * + * @param string $column + * @param string $direction + * @return $this + */ + public function orderByPivot($column, $direction = 'asc') + { + return $this->orderBy($this->qualifyRelationshipColumn($column), $direction); + } + + /** + * Find a related model by its primary key or return a new instance of the related model. + * + * @param mixed $id + * @param array $columns + * @return BaseCollection|Model + */ + public function findOrNew($id, $columns = ['*']) + { + if (is_null($instance = $this->find($id, $columns))) { + $instance = $this->related->newInstance(); + } + + return $instance; + } + + /** + * Get the first related model record matching the attributes or instantiate it. + * + * @return Model + */ + public function firstOrNew(array $attributes = [], array $values = []) + { + if (is_null($instance = $this->related->where($attributes)->first())) { + $instance = $this->related->newInstance(array_merge($attributes, $values)); + } + + return $instance; + } + + /** + * Get the first related record matching the attributes or create it. + * + * @param bool $touch + * @return Model + */ + public function firstOrCreate(array $attributes = [], array $values = [], array $joining = [], $touch = true) + { + if (is_null($instance = (clone $this)->where($attributes)->first())) { + if (is_null($instance = $this->related->where($attributes)->first())) { + $instance = $this->create(array_merge($attributes, $values), $joining, $touch); + } else { + $this->attach($instance, $joining, $touch); + } + } + + return $instance; + } + + /** + * Create or update a related record matching the attributes, and fill it with values. + * + * @param bool $touch + * @return Model + */ + public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true) + { + if (is_null($instance = (clone $this)->where($attributes)->first())) { + if (is_null($instance = $this->related->where($attributes)->first())) { + return $this->create(array_merge($attributes, $values), $joining, $touch); + } else { + $this->attach($instance, $joining, $touch); + } + } + + $instance->fill($values); + + $instance->save(['touch' => false]); + + return $instance; + } + + /** + * Find a related model by its primary key. + * + * @param mixed $id + * @param array $columns + * @return Model|Collection|null + */ + public function find($id, $columns = ['*']) + { + if (! $id instanceof Model && (is_array($id) || $id instanceof Arrayable)) { + return $this->findMany($id, $columns); + } + + return $this->where( + $this->getRelated()->getQualifiedKeyName(), '=', $this->parseId($id) + )->first($columns); + } + + /** + * Find multiple related models by their primary keys. + * + * @param Arrayable|array $ids + * @param array $columns + * @return Collection + */ + public function findMany($ids, $columns = ['*']) + { + $ids = $ids instanceof Arrayable ? $ids->toArray() : $ids; + + if (empty($ids)) { + return $this->getRelated()->newCollection(); + } + + return $this->whereKey( + $this->parseIds($ids) + )->get($columns); + } + + /** + * Find a related model by its primary key or throw an exception. + * + * @param mixed $id + * @param array $columns + * @return Model|Collection + * + * @throws ModelNotFoundException + */ + public function findOrFail($id, $columns = ['*']) + { + $result = $this->find($id, $columns); + + $id = $id instanceof Arrayable ? $id->toArray() : $id; + + if (is_array($id)) { + if (count($result) === count(array_unique($id))) { + return $result; + } + } elseif (! is_null($result)) { + return $result; + } + + throw (new ModelNotFoundException)->setModel(get_class($this->related), $id); + } + + /** + * Find a related model by its primary key or call a callback. + * + * @param mixed $id + * @param Closure|array $columns + * @return Model|Collection|mixed + */ + public function findOr($id, $columns = ['*'], Closure $callback = null) + { + if ($columns instanceof Closure) { + $callback = $columns; + + $columns = ['*']; + } + + $result = $this->find($id, $columns); + + $id = $id instanceof Arrayable ? $id->toArray() : $id; + + if (is_array($id)) { + if (count($result) === count(array_unique($id))) { + return $result; + } + } elseif (! is_null($result)) { + return $result; + } + + return $callback(); + } + + /** + * Add a basic where clause to the query, and return the first result. + * + * @param Closure|string|array $column + * @param mixed $operator + * @param mixed $value + * @param string $boolean + * @return Model|static + */ + public function firstWhere($column, $operator = null, $value = null, $boolean = 'and') + { + return $this->where($column, $operator, $value, $boolean)->first(); + } + + /** + * Execute the query and get the first result. + * + * @param array $columns + * @return mixed + */ + public function first($columns = ['*']) + { + $results = $this->take(1)->get($columns); + + return count($results) > 0 ? $results->first() : null; + } + + /** + * Execute the query and get the first result or throw an exception. + * + * @param array $columns + * @return Model|static + * + * @throws ModelNotFoundException + */ + public function firstOrFail($columns = ['*']) + { + if (! is_null($model = $this->first($columns))) { + return $model; + } + + throw (new ModelNotFoundException)->setModel(get_class($this->related)); + } + + /** + * Execute the query and get the first result or call a callback. + * + * @param Closure|array $columns + * @return Model|static|mixed + */ + public function firstOr($columns = ['*'], Closure $callback = null) + { + if ($columns instanceof Closure) { + $callback = $columns; + + $columns = ['*']; + } + + if (! is_null($model = $this->first($columns))) { + return $model; + } + + return $callback(); + } + + /** + * Get the results of the relationship. + * + * @return mixed + */ + public function getResults() + { + return ! is_null($this->parent->{$this->parentKey}) + ? $this->get() + : $this->related->newCollection(); + } + + /** + * Execute the query as a "select" statement. + * + * @param array $columns + * @return Collection + */ + public function get($columns = ['*']) + { + // First we'll add the proper select columns onto the query so it is run with + // the proper columns. Then, we will get the results and hydrate our pivot + // models with the result of those columns as a separate model relation. + $builder = $this->query->applyScopes(); + + $columns = $builder->getQuery()->columns ? [] : $columns; + + $models = $builder->addSelect( + $this->shouldSelect($columns) + )->getModels(); + + $this->hydratePivotRelation($models); + + // If we actually found models we will also eager load any relationships that + // have been specified as needing to be eager loaded. This will solve the + // n + 1 query problem for the developer and also increase performance. + if (count($models) > 0) { + $models = $builder->eagerLoadRelations($models); + } + + return $this->related->newCollection($models); + } + + /** + * Get the select columns for the relation query. + * + * @return array + */ + protected function shouldSelect(array $columns = ['*']) + { + if ($columns == ['*']) { + $columns = [$this->related->getTable().'.*']; + } + + return array_merge($columns, $this->aliasedPivotColumns()); + } + + /** + * Get the pivot columns for the relation. + * + * "pivot_" is prefixed at each column for easy removal later. + * + * @return array + */ + protected function aliasedPivotColumns() + { + $defaults = [$this->foreignPivotKey, $this->relatedPivotKey]; + + return collect(array_merge($defaults, $this->relationshipColumns))->map(function ($column) { + return $this->qualifyRelationshipColumn($column).' as pivot_'.$column; + })->unique()->all(); + } + + /** + * Get a paginator for the "select" statement. + * + * @param int|null $perPage + * @param array $columns + * @param string $pageName + * @param int|null $page + * @return LengthAwarePaginator + */ + public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) + { + $this->query->addSelect($this->shouldSelect($columns)); + + return tap($this->query->paginate($perPage, $columns, $pageName, $page), function ($paginator) { + $this->hydratePivotRelation($paginator->items()); + }); + } + + /** + * Paginate the given query into a simple paginator. + * + * @param int|null $perPage + * @param array $columns + * @param string $pageName + * @param int|null $page + * @return Paginator + */ + public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) + { + $this->query->addSelect($this->shouldSelect($columns)); + + return tap($this->query->simplePaginate($perPage, $columns, $pageName, $page), function ($paginator) { + $this->hydratePivotRelation($paginator->items()); + }); + } + + /** + * Paginate the given query into a cursor paginator. + * + * @param int|null $perPage + * @param array $columns + * @param string $cursorName + * @param string|null $cursor + * @return CursorPaginator + */ + public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null) + { + $this->query->addSelect($this->shouldSelect($columns)); + + return tap($this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor), function ($paginator) { + $this->hydratePivotRelation($paginator->items()); + }); + } + + /** + * Chunk the results of the query. + * + * @param int $count + * @return bool + */ + public function chunk($count, callable $callback) + { + return $this->prepareQueryBuilder()->chunk($count, function ($results, $page) use ($callback) { + $this->hydratePivotRelation($results->all()); + + return $callback($results, $page); + }); + } + + /** + * Chunk the results of a query by comparing numeric IDs. + * + * @param int $count + * @param string|null $column + * @param string|null $alias + * @return bool + */ + public function chunkById($count, callable $callback, $column = null, $alias = null) + { + $this->prepareQueryBuilder(); + + $column ??= $this->getRelated()->qualifyColumn( + $this->getRelatedKeyName() + ); + + $alias ??= $this->getRelatedKeyName(); + + return $this->query->chunkById($count, function ($results) use ($callback) { + $this->hydratePivotRelation($results->all()); + + return $callback($results); + }, $column, $alias); + } + + /** + * Execute a callback over each item while chunking. + * + * @param int $count + * @return bool + */ + public function each(callable $callback, $count = 1000) + { + return $this->chunk($count, function ($results) use ($callback) { + foreach ($results as $key => $value) { + if ($callback($value, $key) === false) { + return false; + } + } + }); + } + + /** + * Query lazily, by chunks of the given size. + * + * @param int $chunkSize + * @return LazyCollection + */ + public function lazy($chunkSize = 1000) + { + return $this->prepareQueryBuilder()->lazy($chunkSize)->map(function ($model) { + $this->hydratePivotRelation([$model]); + + return $model; + }); + } + + /** + * Query lazily, by chunking the results of a query by comparing IDs. + * + * @param int $chunkSize + * @param string|null $column + * @param string|null $alias + * @return LazyCollection + */ + public function lazyById($chunkSize = 1000, $column = null, $alias = null) + { + $column ??= $this->getRelated()->qualifyColumn( + $this->getRelatedKeyName() + ); + + $alias ??= $this->getRelatedKeyName(); + + return $this->prepareQueryBuilder()->lazyById($chunkSize, $column, $alias)->map(function ($model) { + $this->hydratePivotRelation([$model]); + + return $model; + }); + } + + /** + * Get a lazy collection for the given query. + * + * @return LazyCollection + */ + public function cursor() + { + return $this->prepareQueryBuilder()->cursor()->map(function ($model) { + $this->hydratePivotRelation([$model]); + + return $model; + }); + } + + /** + * Prepare the query builder for query execution. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function prepareQueryBuilder() + { + return $this->query->addSelect($this->shouldSelect()); + } + + /** + * Hydrate the pivot table relationship on the models. + * + * @return void + */ + protected function hydratePivotRelation(array $models) + { + // To hydrate the pivot relationship, we will just gather the pivot attributes + // and create a new Pivot model, which is basically a dynamic model that we + // will set the attributes, table, and connections on it so it will work. + foreach ($models as $model) { + $model->setRelation($this->accessor, $this->newExistingRelation( + $this->migratePivotAttributes($model) + )); + } + } + + /** + * Get the pivot attributes from a model. + * + * @return array + */ + protected function migratePivotAttributes(Model $model) + { + $values = []; + + foreach ($model->getAttributes() as $key => $value) { + // To get the pivots attributes we will just take any of the attributes which + // begin with "pivot_" and add those to this arrays, as well as unsetting + // them from the parent's models since they exist in a different table. + if (str_starts_with($key, 'pivot_')) { + $values[substr($key, 6)] = $value; + + unset($model->$key); + } + } + + return $values; + } + + /** + * If we're touching the parent model, touch. + * + * @return void + */ + public function touchIfTouching() + { + if ($this->touchingParent()) { + $this->getParent()->touch(); + } + + if ($this->getParent()->touches($this->relationName)) { + $this->touch(); + } + } + + /** + * Determine if we should touch the parent on sync. + * + * @return bool + */ + protected function touchingParent() + { + return $this->getRelated()->touches($this->guessInverseRelation()); + } + + /** + * Attempt to guess the name of the inverse of the relation. + * + * @return string + */ + protected function guessInverseRelation() + { + return Str::camel(Str::pluralStudly(class_basename($this->getParent()))); + } + + /** + * Touch all of the related models for the relationship. + * + * E.g.: Touch all roles associated with this user. + * + * @return void + */ + public function touch() + { + $columns = [ + $this->related->getUpdatedAtColumn() => $this->related->freshTimestampString(), + ]; + + // If we actually have IDs for the relation, we will run the query to update all + // the related model's timestamps, to make sure these all reflect the changes + // to the parent models. This will help us keep any caching synced up here. + if (count($ids = $this->allRelatedIds()) > 0) { + $this->getRelated()->newQueryWithoutRelationships()->whereKey($ids)->update($columns); + } + } + + /** + * Get all of the IDs for the related models. + * + * @return BaseCollection + */ + public function allRelatedIds() + { + return $this->newRelationshipQuery()->pluck($this->relatedPivotKey); + } + + /** + * Save a new model and attach it to the parent model. + * + * @param bool $touch + * @return Model + */ + public function save(Model $model, array $pivotAttributes = [], $touch = true) + { + $model->save(['touch' => false]); + + $this->attach($model, $pivotAttributes, $touch); + + return $model; + } + + /** + * Save a new model without raising any events and attach it to the parent model. + * + * @param bool $touch + * @return Model + */ + public function saveQuietly(Model $model, array $pivotAttributes = [], $touch = true) + { + return Model::withoutEvents(function () use ($model, $pivotAttributes, $touch) { + return $this->save($model, $pivotAttributes, $touch); + }); + } + + /** + * Save an array of new models and attach them to the parent model. + * + * @param BaseCollection|array $models + * @return array + */ + public function saveMany($models, array $pivotAttributes = []) + { + foreach ($models as $key => $model) { + $this->save($model, (array) ($pivotAttributes[$key] ?? []), false); + } + + $this->touchIfTouching(); + + return $models; + } + + /** + * Save an array of new models without raising any events and attach them to the parent model. + * + * @param BaseCollection|array $models + * @return array + */ + public function saveManyQuietly($models, array $pivotAttributes = []) + { + return Model::withoutEvents(function () use ($models, $pivotAttributes) { + return $this->saveMany($models, $pivotAttributes); + }); + } + + /** + * Create a new instance of the related model. + * + * @param bool $touch + * @return Model + */ + public function create(array $attributes = [], array $joining = [], $touch = true) + { + $instance = $this->related->newInstance($attributes); + + // Once we save the related model, we need to attach it to the base model via + // through intermediate table so we'll use the existing "attach" method to + // accomplish this which will insert the record and any more attributes. + $instance->save(['touch' => false]); + + $this->attach($instance, $joining, $touch); + + return $instance; + } + + /** + * Create an array of new instances of the related models. + * + * @return array + */ + public function createMany(iterable $records, array $joinings = []) + { + $instances = []; + + foreach ($records as $key => $record) { + $instances[] = $this->create($record, (array) ($joinings[$key] ?? []), false); + } + + $this->touchIfTouching(); + + return $instances; + } + + /** + * Add the constraints for a relationship query. + * + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationExistenceQuery(\Illuminate\Database\Eloquent\Builder $query, Builder $parentQuery, $columns = ['*']) + { + if ($parentQuery->getQuery()->from == $query->getQuery()->from) { + return $this->getRelationExistenceQueryForSelfJoin($query, $parentQuery, $columns); + } + + $this->performJoin($query); + + return parent::getRelationExistenceQuery($query, $parentQuery, $columns); + } + + /** + * Add the constraints for a relationship query on the same table. + * + * @param array|mixed $columns + */ + public function getRelationExistenceQueryForSelfJoin(\Illuminate\Database\Eloquent\Builder $query, \Illuminate\Database\Eloquent\Builder $parentQuery, mixed $columns = ['*']): \Illuminate\Database\Eloquent\Builder + { + $query->select($columns); + + $query->from($this->related->getTable().' as '.$hash = $this->getRelationCountHash()); + + $this->related->setTable($hash); + + $query->whereRelationship($query); // TODO + + return parent::getRelationExistenceQuery($query, $parentQuery, $columns); + } + + /** + * Specify that the pivot table has creation and update timestamps. + * + * @return $this + */ + public function withTimestamps(string|null $createdAt = null, string|null $updatedAt = null): self + { + $this->withTimestamps = true; + + $this->pivotCreatedAtName = $createdAt; + $this->pivotUpdatedAtName = $updatedAt; + + return $this->withPivot($this->createdAt(), $this->updatedAt()); + } + + /** + * Get the name of the "created at" column. + */ + public function createdAt(): string + { + return $this->pivotCreatedAtName ?? $this->parent->getCreatedAtColumn(); + } + + /** + * Get the name of the "updated at" column. + */ + public function updatedAt(): string + { + return $this->pivotUpdatedAtName ?? $this->parent->getUpdatedAtColumn(); + } + + /** + * Get the parent key for the relationship. + */ + public function getParentKeyName(): string + { + return $this->parent->getKeyName(); + } + + /** + * Get the related key for the relationship. + */ + public function getRelatedKeyName(): string + { + return $this->query->getModel()->getKeyName(); + } + + /** + * Get the intermediate table for the relationship. + */ + public function getRelationshipType(): string + { + return $this->relationshipType; + } + + /** + * Get the relationship name for the relationship. + */ + public function getRelationName(): string + { + return $this->relationName; + } + + /** + * Get the name of the pivot accessor for this relationship. + */ + public function getPivotAccessor(): string + { + return $this->accessor; + } + + /** + * Get the pivot columns for this relationship. + * + * @return array + */ + public function getRelationshipColumns() + { + return $this->relationshipColumns; + } + + /** + * Qualify the given column name by the pivot table. + */ + public function qualifyRelationshipColumn(string $column): string + { + return str_contains($column, '.') + ? $column + : $this->relationshipType.'.'.$column; + } + + protected function newRelatedInstanceFor(Model $parent): Model + { + return $this->getRightModel()->newInstance(); + } + + public function getLeftModel(): Model + { + return $this->related; + } + + public function getLeftLabel(): string + { + return $this->getLeftModel()->getTable(); + } + + public function getQualifiedLeftKeyName(): string + { + return $this->getLeftLabel().'.'.$this->getLeftModel()->getKeyName(); + } + + public function getRightModel(): Model + { + return $this->parent; + } + + public function getRightLabel(): string + { + return $this->getRightModel()->getTable(); + } + + public function getQualifiedRightKeyName(): string + { + return $this->getRightLabel().'.'.$this->getRightModel()->getKeyName(); + } +} diff --git a/src/Relations/Relationship.php b/src/Relations/Relationship.php new file mode 100644 index 00000000..3f7a0bcb --- /dev/null +++ b/src/Relations/Relationship.php @@ -0,0 +1,11 @@ +label = $label; - - if (!is_null($callback)) { - $callback($this); - } - } - - /** - * Execute the blueprint against the label. - * - * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Illuminate\Database\Schema\Grammars\Grammar $grammar - */ - public function build(ConnectionInterface $connection, IlluminateSchemaGrammar $grammar) - { - foreach ($this->toCypher($connection, $grammar) as $statement) { - $connection->statement($statement); - } - } - - /** - * Get the raw Cypher statements for the blueprint. - * - * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Illuminate\Database\Schema\Grammars\Grammar $grammar - * - * @return array - */ - public function toCypher(ConnectionInterface $connection, IlluminateSchemaGrammar $grammar) - { - $statements = []; - - // Each type of command has a corresponding compiler function on the schema - // grammar which is used to build the necessary SQL statements to build - // the blueprint element, so we'll just call that compilers function. - foreach ($this->commands as $command) { - $method = 'compile'.ucfirst($command->name); - - if (method_exists($grammar, $method)) { - if (!is_null($cypher = $grammar->$method($this, $command, $connection))) { - $statements = array_merge($statements, (array) $cypher); - } - } - } - - return $statements; - } - - /** - * Indicate that the label should be dropped. - * - * @return \Illuminate\Support\Fluent - */ - public function drop() - { - return $this->addCommand('drop'); - } - - /** - * Indicate that the label should be dropped if it exists. - * - * @return \Illuminate\Support\Fluent - */ - public function dropIfExists() - { - return $this->addCommand('dropIfExists'); - } - - /** - * Rename the label to a given name. - * - * @param string $to - * - * @return \Illuminate\Support\Fluent - */ - public function renameLabel($to) - { - return $this->addCommand('renameLabel', compact('to')); - } - - /** - * Indicate that the given unique constraint on labels properties should be dropped. - * - * @param string|array $properties - * - * @return \Illuminate\Support\Fluent - */ - public function dropUnique($properties) - { - $properties = (array) $properties; - - foreach ($properties as $property) { - $this->indexCommand('dropUnique', $property); - } - } - - /** - * Indicate that the given index on label's properties should be dropped. - * - * @param string|array $properties - * - * @return \Illuminate\Support\Fluent - */ - public function dropIndex($properties) - { - $properties = (array) $properties; - - foreach ($properties as $property) { - $this->indexCommand('dropIndex', $property); - } - } - - /** - * Specify a unique contraint for label's properties. - * - * @param string|array $properties - * - * @return \Illuminate\Support\Fluent - */ - public function unique($properties) - { - $properties = (array) $properties; - - foreach ($properties as $property) { - $this->addCommand('unique', ['property' => $property]); - } - } - - /** - * Specify an index for the label properties. - * - * @param string|array $properties - * - * @return \Illuminate\Support\Fluent - */ - public function index($properties) - { - $properties = (array) $properties; - - foreach ($properties as $property) { - $this->addCommand('index', ['property' => $property]); - } - } - - /** - * Add a new command to the blueprint. - * - * @param string $name - * @param array $parameters - * - * @return \Illuminate\Support\Fluent - */ - protected function addCommand($name, array $parameters = []) - { - $this->commands[] = $command = $this->createCommand($name, $parameters); - - return $command; - } - - /** - * Create a new Fluent command. - * - * @param string $name - * @param array $parameters - * - * @return \Illuminate\Support\Fluent - */ - protected function createCommand($name, array $parameters = []) - { - return new Fluent( - array_merge( - compact('name'), - $parameters) - ); - } - - /** - * Add a new index command to the blueprint. - * - * @param string $type - * @param string|array $property - * @param string $index - * - * @return \Illuminate\Support\Fluent - */ - protected function indexCommand($type, $property) - { - return $this->addCommand($type, compact('property')); - } - - /** - * Set the label that blueprint describes. - * - * @return string - */ - public function setLabel($label) - { - $this->label = $label; - } - - /** - * Get the label that blueprint describes. - * - * @return string - */ - public function getLabel() - { - return $this->label; - } - - /** - * Get the commands on the blueprint. - * - * @return array - */ - public function getCommands() - { - return $this->commands; - } - - /** - * Return the label that blueprint describes. - * - * @return string - */ - public function __toString() - { - return $this->getLabel(); - } -} diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index 9dadb1b8..f2c99149 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -2,210 +2,13 @@ namespace Vinelab\NeoEloquent\Schema; -use Closure; -use Vinelab\NeoEloquent\ConnectionInterface; - -class Builder +class Builder extends \Illuminate\Database\Schema\Builder { /** - * The database connection resolver. - * - * @var \Illuminate\Database\ConnectionInterface - */ - protected $conn; - - /** - * The Blueprint resolver callback. - * - * @var Closure - */ - protected $resolver; - - /** - * @param \Illuminate\Database\ConnectionInterface $conn - */ - public function __construct(ConnectionInterface $conn) - { - $this->conn = $conn; - } - - /** - * Fallback. - * - * @param string $label - * - * @return bool - * - * @throws RuntimeException - */ - public function hasTable($label) - { - throw new \RuntimeException(" -Please use commands from namespace: - neo4j: - neo4j:migrate - neo4j:migrate:make - neo4j:migrate:reset - neo4j:migrate:rollback -If your default database is set to 'neo4j' and you want use other databases side by side with Neo4j -you can do so by passing additional arguments to default migration command like: - php artisan neo4j:migrate --database=other-neo4j - "); - } - - /** - * Create a new data defintion on label schema. - * - * @param string $label - * @param Closure $callback - * - * @return \Vinelab\NeoEloquent\Schema\Blueprint - */ - public function label($label, Closure $callback) - { - return $this->build( - $this->createBlueprint($label, $callback) - ); - } - - /** - * Drop a label from the schema. - * - * @param string $label - * - * @return \Vinelab\NeoEloquent\Schema\Blueprint - */ - public function drop($label) - { - $blueprint = $this->createBlueprint($label); - - $blueprint->drop(); - - return $this->build($blueprint); - } - - /** - * Drop a label from the schema if it exists. - * - * @param string $label - * - * @return \Vinelab\NeoEloquent\Schema\Blueprint - */ - public function dropIfExists($label) - { - $blueprint = $this->createBlueprint($label); - - $blueprint->dropIfExists(); - - return $this->build($blueprint); - } - - /** - * Determine if the given label exists. - * - * @param string $label - * - * @return bool - */ - public function hasLabel($label) - { - $cypher = $this->conn->getSchemaGrammar()->compileLabelExists($label); - - return $this->getConnection()->select($cypher, [])->count() > 0; - } - - /** - * Determine if the given relation exists. - * - * @param string $relation - * - * @return bool - */ - public function hasRelation($relation) - { - $cypher = $this->conn->getSchemaGrammar()->compileRelationExists($relation); - - return $this->getConnection()->select($cypher, [])->count() > 0; - } - - /** - * Rename a label. - * - * @param string $from - * @param string $to - * - * @return \Vinelab\NeoEloquent\Schema\Blueprint|bool - */ - public function renameLabel($from, $to) - { - $blueprint = $this->createBlueprint($from); - - $blueprint->renameLabel($to); - - return $this->build($blueprint); - } - - /** - * Execute the blueprint to modify the label. - * - * @param Blueprint $blueprint - */ - protected function build(Blueprint $blueprint) - { - return $blueprint->build( - $this->getConnection(), - $this->conn->getSchemaGrammar() - ); - } - - /** - * Create a new command set with a Closure. - * - * @param string $label - * @param Closure $callback - * - * @return \Vinelab\NeoEloquent\Schema\Blueprint - */ - protected function createBlueprint($label, Closure $callback = null) - { - if (isset($this->resolver)) { - return call_user_func($this->resolver, $label, $callback); - } else { - return new Blueprint($label, $callback); - } - } - - /** - * Set the database connection instance. - * - * @param \Illuminate\Database\ConnectionResolverInterface - * - * @return \Vinelab\NeoEloquent\Schema\Builder - */ - public function setConnection(ConnectionInterface $connection) - { - $this->conn = $connection; - - return $this; - } - - /** - * Get the database connection instance. - * - * @return \Illuminate\Database\ConnectionResolverInterface - */ - public function getConnection() - { - return $this->conn; - } - - /** - * Set the Schema Blueprint resolver callback. - * - * @param \Closure $resolver + * Drop all tables from the database. */ - public function blueprintResolver(Closure $resolver) + public function dropAllTables(): void { - $this->resolver = $resolver; + $this->getConnection()->affectingStatement('MATCH (x) DETACH DELETE x'); } } diff --git a/src/Schema/Grammars/CypherGrammar.php b/src/Schema/Grammars/CypherGrammar.php index e8346213..b81bce86 100644 --- a/src/Schema/Grammars/CypherGrammar.php +++ b/src/Schema/Grammars/CypherGrammar.php @@ -2,194 +2,454 @@ namespace Vinelab\NeoEloquent\Schema\Grammars; -use Vinelab\NeoEloquent\Schema\Blueprint; +use function array_merge; +use function array_values; +use function collect; +use Illuminate\Database\Connection; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Grammars\Grammar; use Illuminate\Support\Fluent; +use function implode; +use function is_null; +use RuntimeException; +use function sprintf; +use function trim; class CypherGrammar extends Grammar { + public function compileCreateDatabase($name, $connection): string + { + return sprintf('CREATE DATABASE %s', $name); + } + + public function compileDropDatabaseIfExists($name): string + { + return sprintf('DROP DATABASE %s IF EXISTS', $name); + } + /** - * Compile a drop table command. + * Compile the query to determine the list of tables. + */ + public function compileTableExists(): string + { + return <<<'CYPHER' +CALL db.labels() +YIELD label +WHERE label = $0 +RETURN label +CYPHER; + } + + /** + * Compile the query to determine the list of columns. + */ + public function compileColumnListing(string $table): string + { + return << [] + RETURN propertyName as column_name + CYPHER; + } + + /** + * Compile a create table command. + * + * + * @return array + */ + public function compileCreate(Blueprint $blueprint, Fluent $command, Connection $connection) + { + return []; + // $sql = $this->compileCreateTable( + // $blueprint, $command, $connection + // ); + // + // // Once we have the primary SQL, we can add the encoding option to the SQL for + // // the table. Then, we can check if a storage engine has been supplied for + // // the table. If so, we will add the engine declaration to the SQL query. + // $sql = $this->compileCreateEncoding( + // $sql, $connection, $blueprint + // ); + // + // // Finally, we will append the engine configuration onto this SQL statement as + // // the final thing we do before returning this finished SQL. Once this gets + // // added the query will be ready to execute against the real connections. + // return array_values(array_filter(array_merge([$this->compileCreateEngine( + // $sql, $connection, $blueprint + // )], $this->compileAutoIncrementStartingValues($blueprint)))); + } + + /** + * Create the main create table clause. * - * @param Blueprint $blueprint - * @param Fluent $command + * @param Blueprint $blueprint + * @param Fluent $command + * @param Connection $connection + * @return array + */ + protected function compileCreateTable($blueprint, $command, $connection) + { + return trim(sprintf('%s table %s (%s)', + $blueprint->temporary ? 'create temporary' : 'create', + $this->wrapTable($blueprint), + implode(', ', $this->getColumns($blueprint)) + )); + } + + /** + * Append the character set specifications to a command. * + * @param string $sql * @return string */ - public function compileDrop(Blueprint $blueprint, Fluent $command) + protected function compileCreateEncoding($sql, Connection $connection, Blueprint $blueprint) { - $match = $this->compileFrom($blueprint); - $label = $this->prepareLabels(array($blueprint)); + // First we will set the character set if one has been set on either the create + // blueprint itself or on the root configuration for the connection that the + // table is being created on. We will add these to the create table query. + if (isset($blueprint->charset)) { + $sql .= ' default character set '.$blueprint->charset; + } elseif (! is_null($charset = $connection->getConfig('charset'))) { + $sql .= ' default character set '.$charset; + } + + // Next we will add the collation to the create table statement if one has been + // added to either this create table blueprint or the configuration for this + // connection that the query is targeting. We'll add it to this SQL query. + if (isset($blueprint->collation)) { + $sql .= " collate '{$blueprint->collation}'"; + } elseif (! is_null($collation = $connection->getConfig('collation'))) { + $sql .= " collate '{$collation}'"; + } - return $match.' REMOVE n'.$label; + return $sql; } /** - * Compile a drop table (if exists) command. + * Append the engine specifications to a command. + * + * @param string $sql + * @return string + */ + protected function compileCreateEngine($sql, Connection $connection, Blueprint $blueprint) + { + if (isset($blueprint->engine)) { + return $sql.' engine = '.$blueprint->engine; + } elseif (! is_null($engine = $connection->getConfig('engine'))) { + return $sql.' engine = '.$engine; + } + + return $sql; + } + + /** + * Compile an add column command. + * + * + * @return array + */ + public function compileAdd(Blueprint $blueprint, Fluent $command) + { + $columns = $this->prefixArray('add', $this->getColumns($blueprint)); + + return array_values(array_merge( + ['alter table '.$this->wrapTable($blueprint).' '.implode(', ', $columns)], + $this->compileAutoIncrementStartingValues($blueprint) + )); + } + + /** + * Compile the auto-incrementing column starting values. + * + * + * @return array + */ + public function compileAutoIncrementStartingValues(Blueprint $blueprint) + { + return collect($blueprint->autoIncrementingStartingValues())->map(function ($value, $column) use ($blueprint) { + return 'alter table '.$this->wrapTable($blueprint->getTable()).' auto_increment = '.$value; + })->all(); + } + + /** + * Compile a primary key command. * - * @param Blueprint $blueprint - * @param Fluent $command * * @return string */ - public function compileDropIfExists(Blueprint $blueprint, Fluent $command) + public function compilePrimary(Blueprint $blueprint, Fluent $command) + { + $command->name(null); + + return $this->compileKey($blueprint, $command, 'primary key'); + } + + /** + * Compile a unique key command. + * + * + * @return string + */ + public function compileUnique(Blueprint $blueprint, Fluent $command) { - return $this->compileDrop($blueprint, $command); + return $this->compileKey($blueprint, $command, 'unique'); } /** - * Compile the query to determine if the label exists. + * Compile a plain index key command. * - * @var string * * @return string */ - public function compileLabelExists($label) + public function compileIndex(Blueprint $blueprint, Fluent $command) { - $match = $this->compileFrom($label); + return $this->compileKey($blueprint, $command, 'index'); + } - return $match.' RETURN n LIMIT 1;'; + /** + * Compile a fulltext index key command. + * + * + * @return string + */ + public function compileFullText(Blueprint $blueprint, Fluent $command) + { + return $this->compileKey($blueprint, $command, 'fulltext'); } /** - * Compile the query to find the relation. + * Compile a spatial index key command. * - * @var string * * @return string */ - public function compileRelationExists($relation) + public function compileSpatialIndex(Blueprint $blueprint, Fluent $command) { - $relation = mb_strtoupper($this->prepareLabels(array($relation))); + return $this->compileKey($blueprint, $command, 'spatial index'); + } - return "MATCH n-[r$relation]->m RETURN r LIMIT 1"; + /** + * Compile an index creation command. + * + * @param string $type + * @return string + */ + protected function compileKey(Blueprint $blueprint, Fluent $command, $type) + { + return sprintf('alter table %s add %s %s%s(%s)', + $this->wrapTable($blueprint), + $type, + $this->wrap($command->index), + $command->algorithm ? ' using '.$command->algorithm : '', + $this->columnize($command->columns) + ); } /** - * Compile a rename label command. + * Compile a drop table command. * - * @param Blueprint $blueprint - * @param Fluent $command * * @return string */ - public function compileRenameLabel(Blueprint $blueprint, Fluent $command) + public function compileDrop(Blueprint $blueprint, Fluent $command) { - $match = $this->compileFrom($blueprint); - $from = $this->prepareLabels(array($blueprint)); - $to = $this->prepareLabels(array($command->to)); + return 'drop table '.$this->wrapTable($blueprint); + } - return $match." REMOVE n$from SET n$to"; + /** + * Compile a drop table (if exists) command. + * + * + * @return string + */ + public function compileDropIfExists(Blueprint $blueprint, Fluent $command) + { + return 'drop table if exists '.$this->wrapTable($blueprint); } /** - * Compile a unique property command. + * Compile a drop column command. * - * @param Blueprint $blueprint - * @param Fluent $command * * @return string */ - public function compileUnique(Blueprint $blueprint, Fluent $command) + public function compileDropColumn(Blueprint $blueprint, Fluent $command) { - return $this->compileUniqueKey('CREATE', $blueprint, $command); + $columns = $this->prefixArray('drop', $this->wrapArray($command->columns)); + + return 'alter table '.$this->wrapTable($blueprint).' '.implode(', ', $columns); } /** - * Compile a index property command. + * Compile a drop primary key command. * - * @param Blueprint $blueprint - * @param Fluent $command * * @return string */ - public function compileIndex(Blueprint $blueprint, Fluent $command) + public function compileDropPrimary(Blueprint $blueprint, Fluent $command) { - return $this->compileIndexKey('CREATE', $blueprint, $command); + return 'alter table '.$this->wrapTable($blueprint).' drop primary key'; } /** - * Compile a drop unique property command. + * Compile a drop unique key command. * - * @param Blueprint $blueprint - * @param Fluent $command * * @return string */ public function compileDropUnique(Blueprint $blueprint, Fluent $command) { - return $this->compileUniqueKey('DROP', $blueprint, $command); + $index = $this->wrap($command->index); + + return "alter table {$this->wrapTable($blueprint)} drop index {$index}"; } /** - * Compile a drop index property command. + * Compile a drop index command. * - * @param Blueprint $blueprint - * @param Fluent $command * * @return string */ public function compileDropIndex(Blueprint $blueprint, Fluent $command) { - return $this->compileIndexKey('DROP', $blueprint, $command); + $index = $this->wrap($command->index); + + return "alter table {$this->wrapTable($blueprint)} drop index {$index}"; + } + + /** + * Compile a drop fulltext index command. + * + * + * @return string + */ + public function compileDropFullText(Blueprint $blueprint, Fluent $command) + { + return $this->compileDropIndex($blueprint, $command); + } + + /** + * Compile a drop spatial index command. + * + * + * @return string + */ + public function compileDropSpatialIndex(Blueprint $blueprint, Fluent $command) + { + return $this->compileDropIndex($blueprint, $command); } /** - * Compiles index operation. + * Compile a drop foreign key command. * - * @param string $operation - * @param Blueprint $blueprint - * @param Fluent $command * * @return string */ - protected function compileIndexKey($operation, Blueprint $blueprint, Fluent $command) + public function compileDropForeign(Blueprint $blueprint, Fluent $command) { - $label = $this->wrapLabel($blueprint); - $property = $this->propertize($command->property); + $index = $this->wrap($command->index); - return "$operation INDEX ON $label($property)"; + return "alter table {$this->wrapTable($blueprint)} drop foreign key {$index}"; } /** - * Compiles unique operation. + * Compile a rename table command. * - * @param string $operation - * @param Blueprint $blueprint - * @param Fluent $command * * @return string */ - protected function compileUniqueKey($operation, Blueprint $blueprint, Fluent $command) + public function compileRename(Blueprint $blueprint, Fluent $command) { - $label = $this->wrapLabel($blueprint); - $property = $this->propertize($command->property); + $from = $this->wrapTable($blueprint); - return "$operation CONSTRAINT ON (n$label) ASSERT n.$property IS UNIQUE"; + return "rename table {$from} to ".$this->wrapTable($command->to); } /** - * Compile the "from" portion of the query - * which in cypher represents the nodes we're MATCHing. + * Compile a rename index command. * - * @param string $labels * * @return string */ - public function compileFrom($labels) + public function compileRenameIndex(Blueprint $blueprint, Fluent $command) { - // first we will check whether we need - // to reformat the labels from an array - if (is_array($labels)) { - $labels = $this->prepareLabels($labels); - } + return sprintf('alter table %s rename index %s to %s', + $this->wrapTable($blueprint), + $this->wrap($command->from), + $this->wrap($command->to) + ); + } + + /** + * Compile the SQL needed to drop all tables. + * + * @param array $tables + * @return string + */ + public function compileDropAllTables($tables) + { + return 'drop table '.implode(',', $this->wrapArray($tables)); + } + + /** + * Compile the SQL needed to drop all views. + * + * @param array $views + * @return string + */ + public function compileDropAllViews($views) + { + return 'drop view '.implode(',', $this->wrapArray($views)); + } + + /** + * Compile the SQL needed to retrieve all table names. + * + * @return string + */ + public function compileGetAllTables() + { + return 'SHOW FULL TABLES WHERE table_type = \'BASE TABLE\''; + } + + /** + * Compile the SQL needed to retrieve all view names. + * + * @return string + */ + public function compileGetAllViews() + { + return 'SHOW FULL TABLES WHERE table_type = \'VIEW\''; + } - // every label must begin with a ':' so we need to check - // and reformat if need be. - $labels = ':'.preg_replace('/^:/', '', $labels); + /** + * Compile the command to enable foreign key constraints. + * + * @return string + */ + public function compileEnableForeignKeyConstraints() + { + return 'RETURN true as true'; + } - // now we add the default placeholder for this node - $labels = $this->modelAsNode().$labels; + /** + * Compile the command to disable foreign key constraints. + */ + public function compileDisableForeignKeyConstraints(): string + { + return 'RETURN true as true'; + } - return sprintf('MATCH (%s)', $labels); + /** + * Wrap a single string in keyword identifiers. + * + * @param string $value + * @return string + */ + protected function wrapValue($value) + { + throw new RuntimeException('Wrapping of values is not allowed'); } } diff --git a/src/Schema/Grammars/Grammar.php b/src/Schema/Grammars/Grammar.php deleted file mode 100644 index dbab0c54..00000000 --- a/src/Schema/Grammars/Grammar.php +++ /dev/null @@ -1,69 +0,0 @@ - $value) { - $recordsByKeys[$key] = $recordsByKeys[$key] ?? []; - $recordsByKeys[$key][] = $value; - } - } - - return $recordsByKeys; - } - - public function getRelationshipRecords(CypherList $results): array - { - $relationships = []; - - foreach ($results as $record) { - $relationships = array_merge($relationships, $this->getRecordRelationships($record)); - } - - return $relationships; - } - - public function getNodeRecords(CypherList $result): array - { - $nodes = []; - - foreach ($result as $record) { - $nodes = array_merge($nodes, $this->getRecordNodes($record)); - } - - return $nodes; - } - - /** - * @param CypherList $result - * @return mixed - */ - public function getSingleItem(CypherList $result) - { - /** @var CypherMap $map */ - $map = $result->first(); - return $map->first()->getValue(); - } - - public function getNodeByType(Relationship $relation, array $nodes, string $type = 'start'): Node - { - if($type === 'start') { - $id = $relation->getStartNodeId(); - } else { - $id = $relation->getEndNodeId(); - } - - /** @var Node $node */ - foreach ($nodes as $node) { - if($id === $node->getId()) { - return $node; - } - } - - throw new RuntimeException('Cannot find node with id: ' . $node->getId()); - } - - /** - * @return list - */ - public function getRecordNodes(CypherMap $record): array - { - $nodes = []; - - foreach ($record as $value) { - if($value instanceof Node) { - $nodes[] = $value; - } - } - - return $nodes; - } - - /** - * @return list - */ - public function getRecordRelationships(CypherMap $record): array - { - $relationships = []; - - foreach ($record as $item) { - if($item instanceof Relationship) { - $relationships[] = $item; - } - } - - return $relationships; - } -} \ No newline at end of file diff --git a/tests/Fixtures/Location.php b/tests/Fixtures/Location.php new file mode 100644 index 00000000..5267f424 --- /dev/null +++ b/tests/Fixtures/Location.php @@ -0,0 +1,20 @@ +morphTo(); + } +} diff --git a/tests/Fixtures/Permission.php b/tests/Fixtures/Permission.php new file mode 100644 index 00000000..3ef0cf4c --- /dev/null +++ b/tests/Fixtures/Permission.php @@ -0,0 +1,20 @@ +belongsToMany(Role::class, 'belongsToMany(User::class); + } + + public function permissions(): HasMany + { + return $this->hasMany(Permission::class); + } + + public function relatedUser(): BelongsToMany + { + return $this->belongsToMany(User::class, 'belongsToMany(Permission::class, 'HAS_PERMISSION>'); + } +} diff --git a/tests/Fixtures/User.php b/tests/Fixtures/User.php new file mode 100644 index 00000000..dd3143fe --- /dev/null +++ b/tests/Fixtures/User.php @@ -0,0 +1,71 @@ +belongsTo(Location::class); + } + + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class); + } + + public function profile(): HasOne + { + return $this->hasOne(Profile::class); + } + + public function firstInRole(): HasOneThrough + { + return $this->hasOneThrough(Role::class, User::class); + } + + public function sameRoles(): HasManyThrough + { + return $this->hasManyThrough(Role::class, User::class); + } + + public function posts(): MorphToMany + { + return $this->morphToMany(Post::class, 'postable'); + } + + public function morphToLocation(): MorphTo + { + return $this->morphTo(Location::class, 'locatable'); + } + + public function colleagues(): HasMany + { + return $this->hasMany(User::class); + } + + public function relatedRoles(): BelongsToMany + { + return $this->belongsToMany(Role::class, table: 'HAS_ROLE>'); + } +} diff --git a/tests/Functional/AggregateTest.php b/tests/Functional/AggregateTest.php new file mode 100644 index 00000000..b8397bfb --- /dev/null +++ b/tests/Functional/AggregateTest.php @@ -0,0 +1,307 @@ +getConnection()->affectingStatement('MATCH (x) DETACH DELETE x'); + + $this->builder = new Builder($this->getConnection()); + } + + public function testCount(): void + { + User::query()->create([]); + $this->assertEquals(1, User::query()->count()); + User::query()->create([]); + $this->assertEquals(2, User::query()->count()); + User::query()->create([]); + $this->assertEquals(3, User::query()->count()); + + User::query()->create(['logins' => 10]); + $this->assertEquals(1, User::query()->count('logins')); + + User::query()->create(['logins' => 10]); + $this->assertEquals(2, User::query()->count('logins')); + + User::query()->create(['points' => 200]); + $this->assertEquals(1, User::query()->count('points')); + } + + public function testCountWithQuery(): void + { + User::query()->create(['email' => 'foo@mail.net', 'points' => 2]); + User::query()->create(['email' => 'bar@mail.net', 'points' => 2]); + + $count = User::query()->where('email', 'foo@mail.net')->count(); + $this->assertEquals(1, $count); + + $count = User::query()->where('email', 'bar@mail.net')->count(); + $this->assertEquals(1, $count); + + $count = User::query()->where('points', 2)->count(); + $this->assertEquals(2, $count); + } + + public function testCountDistinct(): void + { + User::query()->create(['logins' => 1]); + User::query()->create(['logins' => 2]); + User::query()->create(['logins' => 2]); + User::query()->create(['logins' => 3]); + User::query()->create(['logins' => 3]); + User::query()->create(['logins' => 4]); + User::query()->create(['logins' => 4]); + + $this->assertEquals( + 4, + User::query()->distinct()->count('logins') + ); + } + + public function testCountDistinctWithQuery(): void + { + User::query()->create(['logins' => 1]); + User::query()->create(['logins' => 2]); + User::query()->create(['logins' => 2]); + User::query()->create(['logins' => 3]); + User::query()->create(['logins' => 3]); + User::query()->create(['logins' => 4]); + User::query()->create(['logins' => 4]); + + $count = User::query()->where('logins', '>', 2)->distinct()->count('logins'); + $this->assertEquals(2, $count); + } + + public function testMax(): void + { + User::query()->create(['logins' => 10, 'points' => 1]); + User::query()->create(['logins' => 11, 'points' => 4]); + User::query()->create(['logins' => 12, 'points' => 2]); + + $this->assertEquals(12, User::query()->max('logins')); + $this->assertEquals(4, User::query()->max('points')); + } + + public function testMaxWithQuery(): void + { + User::query()->create(['logins' => 10, 'points' => 1]); + User::query()->create(['logins' => 11, 'points' => 2]); + User::query()->create(['logins' => 12, 'points' => 4]); + + $count = User::query()->where('points', '<', 4)->max('logins'); + $this->assertEquals(11, $count); + $this->assertEquals(2, User::query()->where('points', '<', 4)->max('points')); + } + + public function testMin(): void + { + User::query()->create(['logins' => 10, 'points' => 1]); + User::query()->create(['logins' => 11, 'points' => 4]); + User::query()->create(['logins' => 12, 'points' => 2]); + + $this->assertEquals(10, User::query()->min('logins')); + $this->assertEquals(1, User::query()->min('points')); + } + + public function testMinWithQuery(): void + { + User::query()->create(['logins' => 10, 'points' => 1]); + User::query()->create(['logins' => 11, 'points' => 4]); + User::query()->create(['logins' => 12, 'points' => 2]); + + $query = User::query()->where('points', '>', 1); + $this->assertEquals(11, $query->min('logins')); + $this->assertEquals(2, $query->min('points')); + } + + public function testAvg(): void + { + User::query()->create(['logins' => 10, 'points' => 1]); + User::query()->create(['logins' => 11, 'points' => 4]); + User::query()->create(['logins' => 12, 'points' => 2]); + + $this->assertEquals(11, User::query()->avg('logins')); + $this->assertEquals(2.3333333333333335, User::query()->avg('points')); + } + + public function testAvgWithQuery(): void + { + User::query()->create(['logins' => 10, 'points' => 1]); + User::query()->create(['logins' => 11, 'points' => 4]); + User::query()->create(['logins' => 12, 'points' => 2]); + + $query = User::query()->where('points', '>', 1); + + $this->assertEquals(11.5, $query->avg('logins')); + $this->assertEquals(3, $query->avg('points')); + } + + public function testSum(): void + { + User::query()->create(['logins' => 10, 'points' => 1]); + User::query()->create(['logins' => 11, 'points' => 4]); + User::query()->create(['logins' => 12, 'points' => 2]); + + $this->assertEquals(33, User::query()->sum('logins')); + $this->assertEquals(7, User::query()->sum('points')); + } + + public function testSumWithQuery(): void + { + User::query()->create(['logins' => 10, 'points' => 1]); + User::query()->create(['logins' => 11, 'points' => 4]); + User::query()->create(['logins' => 12, 'points' => 2]); + + $query = User::query()->where('points', '>', 1); + $this->assertEquals(23, $query->sum('logins')); + $this->assertEquals(6, $query->sum('points')); + } + + public function testPercentileDisc(): void + { + User::query()->create(['logins' => 10, 'points' => 1]); + User::query()->create(['logins' => 11, 'points' => 4]); + User::query()->create(['logins' => 12, 'points' => 2]); + + $this->assertEquals(11, User::query()->percentileDisc('logins', 0.5)); + $this->assertEquals(12, User::query()->percentileDisc('logins', 1)); + + $this->assertEquals(1, User::query()->percentileDisc('points')); + $this->assertEquals(2, (int) User::query()->percentileDisc('points', 0.6)); + $this->assertEquals(4, User::query()->percentileDisc('points', 0.9)); + } + + public function testPercentileDiscWithQuery(): void + { + User::query()->create(['logins' => 10, 'points' => 1]); + User::query()->create(['logins' => 11, 'points' => 4]); + User::query()->create(['logins' => 12, 'points' => 2]); + + $builder = User::query()->where('points', '>', 1); + $this->assertEquals(11, $builder->percentileDisc('logins')); + $this->assertEquals(11, $builder->percentileDisc('logins', 0.5)); + $this->assertEquals(12, $builder->percentileDisc('logins', 1)); + $this->assertEquals(2, $builder->percentileDisc('points')); + $this->assertEquals(4.0, $builder->percentileDisc('points', 0.6)); + $this->assertEquals(4, $builder->percentileDisc('points', 0.9)); + } + + public function testPercentileCont(): void + { + User::query()->create(['logins' => 10, 'points' => 1]); + User::query()->create(['logins' => 11, 'points' => 4]); + User::query()->create(['logins' => 12, 'points' => 2]); + + $this->assertEquals(10, User::query()->percentileCont('logins'), 0.2); + $this->assertEquals(10.800000000000001, User::query()->percentileCont('logins', 0.4)); + $this->assertEquals(11.800000000000001, User::query()->percentileCont('logins', 0.9)); + + $this->assertEquals(1, User::query()->percentileCont('points'), 0.3); + $this->assertEquals(2.3999999999999999, User::query()->percentileCont('points', 0.6)); + $this->assertEquals(3.6000000000000001, User::query()->percentileCont('points', 0.9)); + } + + public function testPercentileContWithQuery(): void + { + User::query()->create(['logins' => 10, 'points' => 1]); + User::query()->create(['logins' => 11, 'points' => 4]); + User::query()->create(['logins' => 12, 'points' => 2]); + + $builder = User::query(); + $builder->where('points', '<', 4); + $this->assertEquals(10.4, $builder->percentileCont('logins', 0.2)); + $this->assertEquals(10.8, $builder->percentileCont('logins', 0.4)); + $this->assertEquals(11.8, $builder->percentileCont('logins', 0.9)); + + $this->assertEquals(1.2999999999999998, $builder->percentileCont('points', 0.3)); + $this->assertEquals(1.6, $builder->percentileCont('points', 0.6)); + $this->assertEquals(1.8999999999999999, $builder->percentileCont('points', 0.9)); + } + + public function testStdev(): void + { + User::query()->create(['logins' => 33, 'points' => 1]); + User::query()->create(['logins' => 44, 'points' => 4]); + User::query()->create(['logins' => 55, 'points' => 2]); + + $this->assertEquals(11, User::query()->stdev('logins')); + $this->assertEqualsWithDelta(1.52, User::query()->stdev('points'), 0.01); + } + + public function testStdevWithQuery(): void + { + User::query()->create(['logins' => 33, 'points' => 1]); + User::query()->create(['logins' => 44, 'points' => 4]); + User::query()->create(['logins' => 55, 'points' => 2]); + + $query = User::query()->where('points', '>', 1); + $this->assertEqualsWithDelta(7.78, $query->stdev('logins'), 0.01); + $this->assertEqualsWithDelta(1.41, $query->stdev('points'), 0.01); + } + + public function testStdevp(): void + { + User::query()->create(['logins' => 33, 'points' => 1]); + User::query()->create(['logins' => 44, 'points' => 4]); + User::query()->create(['logins' => 55, 'points' => 2]); + + $this->assertEqualsWithDelta(8.98, User::query()->stdevp('logins'), 0.01); + $this->assertEqualsWithDelta(1.25, User::query()->stdevp('points'), 0.01); + } + + public function testStdevpWithQuery(): void + { + User::query()->create(['logins' => 33, 'points' => 1]); + User::query()->create(['logins' => 44, 'points' => 4]); + User::query()->create(['logins' => 55, 'points' => 2]); + + $query = User::query(); + $query->where('points', '>', 1); + $this->assertEqualsWithDelta(5.5, $query->stdevp('logins'), 0.01); + $this->assertEqualsWithDelta(1, $query->stdevp('points'), 0.01); + } + + public function testCollect(): void + { + User::query()->create(['logins' => 33, 'points' => 1]); + User::query()->create(['logins' => 44, 'points' => 4]); + User::query()->create(['logins' => 55, 'points' => 2]); + + $logins = User::query()->collect('logins'); + $this->assertInstanceOf(Collection::class, $logins); + $this->assertCount(3, $logins); + $this->assertContains(33, $logins); + $this->assertContains(44, $logins); + $this->assertContains(55, $logins); + + $points = User::query()->collect('points'); + $this->assertInstanceOf(Collection::class, $points); + $this->assertCount(3, $points); + $this->assertContains(1, $points); + $this->assertContains(4, $points); + $this->assertContains(2, $points); + } + + public function testCollectWithQuery(): void + { + User::query()->create(['logins' => 33, 'points' => 1]); + User::query()->create(['logins' => 44, 'points' => 4]); + User::query()->create(['logins' => 55, 'points' => 2]); + + $logins = User::query()->where('points', '>', 1)->collect('logins'); + $this->assertInstanceOf(Collection::class, $logins); + + $this->assertCount(2, $logins); + $this->assertContains(44, $logins); + $this->assertContains(55, $logins); + } +} diff --git a/tests/Functional/BelongsToManyRelationTest.php b/tests/Functional/BelongsToManyRelationTest.php new file mode 100644 index 00000000..cf315490 --- /dev/null +++ b/tests/Functional/BelongsToManyRelationTest.php @@ -0,0 +1,244 @@ +getConnection()->affectingStatement('MATCH (x) DETACH DELETE x'); + } + + public function testSavingRelatedBelongsToMany(): void + { + $user = User::create(['uuid' => '11213', 'name' => 'Creepy Dude']); + $role = new Role(['title' => 'Master']); + + $user->roles()->save($role); + + $this->assertCount(1, $user->roles); + $this->assertCount(1, $role->users); + } + + public function testAttachingModelId() + { + $user = User::create(['uuid' => '4622', 'name' => 'Creepy Dude']); + $role = Role::create(['title' => 'Master']); + $user->roles()->attach($role->getKey()); + + $this->assertCount(1, $user->roles); + } + + public function testAttachingManyModelIds() + { + $user = User::create(['uuid' => '64753', 'name' => 'Creepy Dude']); + $master = Role::create(['title' => 'Master']); + $admin = Role::create(['title' => 'Admin']); + $editor = Role::create(['title' => 'Editor']); + + $user->roles()->attach([$master->getKey(), $admin->getKey(), $editor->getKey()]); + + $this->assertCount(3, $user->roles); + $roles = $user->roles; + $this->assertEqualsCanonicalizing(['Master', 'Admin', 'Editor'], $user->roles->pluck('title')->toArray()); + } + + public function testAttachingModelInstance() + { + $user = User::create(['uuid' => '19583', 'name' => 'Creepy Dude']); + $role = Role::create(['title' => 'Master']); + + $user->roles()->attach($role); + + $this->assertTrue($user->roles->first()->is($role)); + $this->assertTrue($role->users->first()->is($user)); + } + + public function testDetachingModelById() + { + $user = User::create(['uuid' => '943543', 'name' => 'Creepy Dude']); + $role = Role::create(['title' => 'Master']); + + $user->roles()->attach($role->getKey()); + $user = User::find($user->getKey()); + + $this->assertCount(1, $user->roles); + + $user->roles()->detach($role->getKey()); + $user = User::find($user->getKey()); + + $this->assertCount(0, $user->roles); + } + + public function testDetachingManyModelIds() + { + $user = User::create(['uuid' => '8363', 'name' => 'Creepy Dude']); + $master = Role::create(['title' => 'Master']); + $admin = Role::create(['title' => 'Admin']); + $editor = Role::create(['title' => 'Editor']); + + $user->roles()->attach([$master->getKey(), $admin->getKey(), $editor->getKey()]); + $user = User::find($user->getKey()); + + $this->assertCount(3, $user->roles); + $user = User::find($user->getKey()); + + $user->roles()->detach(); + $this->assertCount(0, $user->roles); + } + + public function testSyncingModelIds() + { + $user = User::create(['uuid' => '25467', 'name' => 'Creepy Dude']); + $master = Role::create(['title' => 'Master']); + $admin = Role::create(['title' => 'Admin']); + $editor = Role::create(['title' => 'Editor']); + + $user->roles()->attach($master->getKey()); + + $user->roles()->sync([$admin->getKey(), $editor->getKey()]); + + $edgesIds = $user->roles->pluck('title')->toArray(); + + $this->assertTrue(in_array($admin->getKey(), $edgesIds)); + $this->assertTrue(in_array($editor->getKey(), $edgesIds)); + $this->assertFalse(in_array($master->getKey(), $edgesIds)); + } + + public function testSyncingUpdatesModels() + { + $user = User::create(['uuid' => '14285', 'name' => 'Creepy Dude']); + $master = Role::create(['title' => 'Master']); + $admin = Role::create(['title' => 'Admin']); + $editor = Role::create(['title' => 'Editor']); + + $user->roles()->attach($master->getKey()); + $user = User::find($user->getKey()); + $this->assertCount(1, $user->roles); + + $user->roles()->sync([$master->getKey(), $admin->getKey(), $editor->getKey()]); + $user = User::find($user->getKey()); + + $edges = $user->roles->pluck('title')->toArray(); + + $this->assertCount(3, $edges); + $this->assertTrue(in_array($admin->getKey(), $edges)); + $this->assertTrue(in_array($editor->getKey(), $edges)); + $this->assertTrue(in_array($master->getKey(), $edges)); + } + + public function testSyncingWithAttributes() + { + $user = User::create(['uuid' => '83532', 'name' => 'Creepy Dude']); + $master = Role::create(['title' => 'Master']); + $admin = Role::create(['title' => 'Admin']); + $editor = Role::create(['title' => 'Editor']); + + $user->roles()->attach($master->getKey()); + + $user->roles()->sync([ + $master->getKey() => ['type' => 'Master'], + $admin->getKey() => ['type' => 'Admin'], + $editor->getKey() => ['type' => 'Editor'], + ]); + + $edges = $user->roles() + ->withPivot('type') + ->orderBy('title') + ->select(['title']) + ->get() + ->pluck('title', 'pivot.type') + ->toArray(); + + $this->assertEquals(['Admin' => 'Admin', 'Editor' => 'Editor', 'Master' => 'Master'], $edges); + } + + public function testEagerLoadingBelongsToMany() + { + $user = User::create(['uuid' => '44352', 'name' => 'Creepy Dude']); + $master = Role::create(['title' => 'Master']); + $admin = Role::create(['title' => 'Admin']); + $editor = Role::create(['title' => 'Editor']); + + $user->roles()->attach([$master->getKey(), $admin->getKey(), $editor->getKey()]); + + $creep = User::with('roles')->find($user->getKey()); + $relations = $creep->getRelations(); + + $this->assertArrayHasKey('roles', $relations); + $this->assertCount(3, $relations['roles']); + } + + /** + * Regression for issue #120. + * + * @see https://github.com/Vinelab/NeoEloquent/issues/120 + */ + public function testDeletingBelongsToManyRelation() + { + $user = User::create(['uuid' => '34113', 'name' => 'Creepy Dude']); + $master = Role::create(['title' => 'Master']); + $admin = Role::create(['title' => 'Admin']); + $editor = Role::create(['title' => 'Editor']); + + $user->roles()->attach([$master->getKey(), $admin->getKey(), $editor->getKey()]); + + $fetched = User::find($user->getKey()); + $this->assertCount(3, $user->roles, 'relations created successfully'); + + $deleted = $fetched->roles()->detach(); + $this->assertTrue((bool) $deleted); + + $again = User::find($user->getKey()); + $this->assertCount(0, $again->roles); + + $masterDeleted = Role::where('title', 'Master')->first(); + $this->assertNotNull($masterDeleted); + + $adminDeleted = Role::where('title', 'Admin')->first(); + $this->assertNotNull($adminDeleted); + + $editorDeleted = Role::where('title', 'Edmin')->first(); + $this->assertNull($editorDeleted); + } + + /** + * Regression for issue #120. + * + * @see https://github.com/Vinelab/NeoEloquent/issues/120 + */ + public function testDeletingBelongsToManyRelationKeepingEndModels() + { + $user = User::create(['uuid' => '84633', 'name' => 'Creepy Dude']); + $master = Role::create(['title' => 'Master']); + $admin = Role::create(['title' => 'Admin']); + $editor = Role::create(['title' => 'Editor']); + + $user->roles()->attach([$master->getKey(), $admin->getKey(), $editor->getKey()]); + + $fetched = User::find($user->getKey()); + $this->assertCount(3, $user->roles, 'relations created successfully'); + + $deleted = $fetched->roles()->detach(); + $this->assertTrue((bool) $deleted); + + $again = User::find($user->getKey()); + $this->assertCount(0, $again->roles); + + // roles should've been deleted too. + $masterDeleted = Role::find($master->getKey()); + $this->assertEquals($master->toArray(), $masterDeleted->toArray()); + + $adminDeleted = Role::find($admin->getKey()); + $this->assertEquals($admin->toArray(), $adminDeleted->toArray()); + + $editorDeleted = Role::find($editor->getKey()); + $this->assertEquals($editor->toArray(), $editorDeleted->toArray()); + } +} diff --git a/tests/Functional/BelongsToRelationTest.php b/tests/Functional/BelongsToRelationTest.php new file mode 100644 index 00000000..48b99cf3 --- /dev/null +++ b/tests/Functional/BelongsToRelationTest.php @@ -0,0 +1,66 @@ + 89765, + 'long' => -876521234, + 'country' => 'The Netherlands', + 'city' => 'Amsterdam', + ]); + $user = User::create([ + 'name' => 'Daughter', + 'alias' => 'daughter', + ]); + + $user->location()->associate($location); + $user->save(); + + $fetched = User::first(); + $this->assertEquals($location->toArray(), $fetched->location->toArray()); + + $fetched->location()->disassociate(); + $fetched->save(); + + $fetched = User::first(); + + $this->assertNull($fetched->location); + } + + public function testDynamicLoadingBelongsToFromFoundRecord(): void + { + $location = Location::create(['lat' => 89765, 'long' => -876521234, 'country' => 'The Netherlands', 'city' => 'Amsterdam']); + /** @var \Vinelab\NeoEloquent\Tests\Functional\Fixtures\User $user */ + $user = User::create(['name' => 'Daughter', 'alias' => 'daughter']); + $user->location()->associate($location); + $user->save(); + + $found = User::query()->find($user->getKey()); + + $this->assertEquals($location->toArray(), $found->location->toArray()); + } + + public function testEagerLoadingBelongsTo(): void + { + $location = Location::create(['lat' => 89765, 'long' => -876521234, 'country' => 'The Netherlands', 'city' => 'Amsterdam']); + $user = User::create(['name' => 'Daughter', 'alias' => 'daughter']); + $user->location()->associate($location); + $user->save(); + + $relations = User::with('location')->find($user->getKey())->getRelations(); + + $this->assertArrayHasKey('location', $relations); + $this->assertEquals($location->toArray(), $relations['location']->toArray()); + } +} diff --git a/tests/Functional/BuilderTest.php b/tests/Functional/BuilderTest.php new file mode 100644 index 00000000..295574c8 --- /dev/null +++ b/tests/Functional/BuilderTest.php @@ -0,0 +1,164 @@ +getConnection()->affectingStatement('MATCH (x) DETACH DELETE x'); + + $this->builder = new Builder($this->getConnection()); + } + + public function testDBIntegration(): void + { + self::assertInstanceOf(Builder::class, DB::table('Node')); + } + + public function testSettingNodeLabels(): void + { + $this->builder->from('labels'); + $this->assertEquals('labels', $this->builder->from); + + $this->builder->from('User:Fan'); + $this->assertEquals('User:Fan', $this->builder->from); + } + + public function testInsertingAndGettingId(): void + { + $this->builder->from('Hero'); + + $values = [ + 'length' => 123, + 'height' => 343, + 'power' => 'Strong Fart Noises', + 'id' => 69, + ]; + + $hero = $this->builder->insertGetId($values); + $this->assertInstanceOf(Node::class, $hero); + $this->assertEquals(123, $hero->getProperty('length')); + $this->assertEquals(343, $hero->getProperty('height')); + $this->assertEquals('Strong Fart Noises', $hero->getProperty('power')); + $this->assertEquals(69, $hero->getProperty('id')); + } + + public function testBatchInsert(): void + { + $this->builder->from('Hero')->insert([ + ['a' => 'b', 'c' => 'd'], + ['c' => 'd', 'a' => 'a'], + ]); + + $results = $this->builder->orderBy('a')->get(); + self::assertEquals([ + ['c' => 'd', 'a' => 'a'], + ['a' => 'b', 'c' => 'd'], + ], $results->toArray()); + } + + public function testUpsert(): void + { + $this->markTestSkipped('Upsert not supported yet'); + // $this->builder->from('Hero')->upsert([ + // ['a' => 'aa', 'b' => 'bb', 'c' => 'cc'], + // ['a' => 'aaa', 'b' => 'bbb', 'c' => 'ccc'], + // ], ['a'], ['c']); + // + // self::assertEqualsCanonicalizing([ + // ['a' => 'aa', 'b' => 'bb', 'c' => 'cc'], + // ['a' => 'aaa', 'b' => 'bbb', 'c' => 'ccc'], + // ], $this->builder->get()->toArray()); + // + // $this->builder->from('Hero')->upsert([ + // ['a' => 'aa', 'b' => 'bb', 'c' => 'cdc'], + // ['a' => 'aaa', 'b' => 'bbb', 'c' => 'ccdc'], + // ], ['a'], ['c']); + // + // self::assertEqualsCanonicalizing([ + // ['a' => 'aa', 'b' => 'bb', 'c' => 'cdc'], + // ['a' => 'aaa', 'b' => 'bbb', 'c' => 'ccdc'], + // ], $this->builder->get()->toArray()); + } + + public function testFailingWhereWithNullValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->builder->where('id', '>', null); + } + + public function testBasicWhereBindings(): void + { + $this->builder->where('id', 19); + + $this->assertEquals([ + [ + 'type' => 'Basic', + 'column' => 'id', + 'operator' => '=', + 'value' => 19, + 'boolean' => 'and', + ], + ], $this->builder->wheres, 'make sure the statement was atted to $wheres'); + } + + public function testBasicWhereBindingsWithFromField(): void + { + $this->builder->from = ['user']; + $this->builder->where('id', 19); + + $this->assertEquals([ + [ + 'type' => 'Basic', + 'column' => 'id', + 'operator' => '=', + 'value' => 19, + 'boolean' => 'and', + ], + ], $this->builder->wheres); + } + + public function testNullWhereBindings(): void + { + $this->builder->where('farted', null); + + $this->assertEquals([ + [ + 'type' => 'Null', + 'boolean' => 'and', + 'column' => 'farted', + ], + ], $this->builder->wheres); + } + + public function testWhereTransformsNodeIdBinding(): void + { + // when requesting a Node by its id we need to use + // 'id(n)' but that won't be helpful when returned or dealt with + // so we need to tranform it back to 'id' + $this->builder->where('id(n)', 200); + + $this->assertEquals([ + [ + 'type' => 'Basic', + 'column' => 'id(n)', + 'boolean' => 'and', + 'operator' => '=', + 'value' => 200, + ], + ], $this->builder->wheres); + } + + protected function getBuilder(): Builder + { + return new Builder($this->getConnection()); + } +} diff --git a/tests/Functional/ConnectionTest.php b/tests/Functional/ConnectionTest.php new file mode 100644 index 00000000..c5b727ca --- /dev/null +++ b/tests/Functional/ConnectionTest.php @@ -0,0 +1,246 @@ + 'A', + 'email' => 'ABC@efg.com', + 'username' => 'H I', + ]; + + public function testRegisteredConnectionResolver(): void + { + $resolver = Model::getConnectionResolver(); + + self::assertInstanceOf(DatabaseManager::class, $resolver); + self::assertEquals('neo4j', $resolver->getDefaultConnection()); + self::assertInstanceOf(Connection::class, $resolver->connection('neo4j')); + self::assertInstanceOf(Connection::class, $resolver->connection('default')); + + self::assertEquals('neo4j', $resolver->connection('neo4j')->getDatabaseName()); + self::assertEquals('neo4j', $resolver->connection('default')->getDatabaseName()); + } + + public function testLogQueryFiresEventsIfSet(): void + { + $connection = $this->getConnection(); + + $connection->logQuery('foo', [], time()); + + $events = M::mock(Dispatcher::class); + $connection->setEventDispatcher($events); + $events->shouldReceive('dispatch')->once()->withArgs(function ($x) use ($connection) { + self::assertEquals($x, new QueryExecuted('foo', [], null, $connection)); + + return true; + }); + + $connection->logQuery('foo', []); + } + + public function testPretendOnlyLogsQueries(): void + { + $connection = $this->getConnection(); + $connection->enableQueryLog(); + $queries = $connection->pretend(function ($connection) { + $connection->select('foo bar', ['baz']); + }); + $this->assertEquals('foo bar', $queries[0]['query']); + $this->assertEquals(['baz'], $queries[0]['bindings']); + } + + public function testPreparingSimpleBindings(): void + { + $bindings = [ + 'param0' => 'jd', + 'param1' => 'John Doe', + ]; + + $prepared = $this->getConnection('default')->prepareBindings($bindings); + + $this->assertEquals($bindings, $prepared); + } + + public function testPreparingWheresBindings(): void + { + $bindings = [ + 'username' => 'jd', + 'email' => 'marie@curie.sci', + ]; + + /** @var Connection $c */ + $c = $this->getConnection('default'); + + $expected = [ + 'username' => 'jd', + 'email' => 'marie@curie.sci', + ]; + + $prepared = $c->prepareBindings($bindings); + + $this->assertEquals($expected, $prepared); + } + + public function testAffectingStatement(): void + { + $c = $this->getConnection('default'); + + $this->createUser(); + + $type = 'dev'; + + // Now we update the type and set it to $type + $query = 'MATCH (n:`User`) WHERE n.username = $param0 '. + 'SET n.type = $param1, n.updated_at = $param2 '. + 'RETURN count(n)'; + + $bindings = [ + 'param0' => $this->user['username'], + 'param1' => $type, + 'param2' => '2014-05-11 13:37:15', + ]; + + $c->affectingStatement($query, $bindings); + + // Try to find the updated one and make sure it was updated successfully + $query = 'MATCH (n:User) WHERE n.username = $param0 RETURN n'; + + $results = $this->getConnection()->select($query, $bindings); + + $user = $results[0]['n']->getProperties()->toArray(); + + $this->assertEquals($type, $user['type']); + } + + public function testAffectingStatementOnNonExistingRecord(): void + { + $c = $this->getConnection(); + + $type = 'dev'; + + // Now we update the type and set it to $type + $query = 'MATCH (n:`User`) WHERE n.username = $param0 '. + 'SET n.type = $param1, n.updated_at = $param2 '. + 'RETURN count(n)'; + + $bindings = [ + 'param0' => $this->user['username'], + 'param1' => $type, + 'param2' => '2014-05-11 13:37:15', + ]; + + $result = $c->affectingStatement($query, $bindings); + + self::assertGreaterThan(0, $result); + + $this->createUser(); + + $result = $c->affectingStatement($query, $bindings); + + self::assertGreaterThan(0, $result); + } + + public function testSelectOneCallsSelectAndReturnsSingleResult(): void + { + $connection = $this->getConnection(); + + $this->createUser(); + $this->createUser(); + + $this->assertInstanceOf(Node::class, $connection->selectOne('MATCH (x) RETURN x')['x']); + } + + public function testBeganTransactionFiresEventsIfSet(): void + { + $connection = $this->getConnection(); + + $events = M::mock(Dispatcher::class); + $connection->setEventDispatcher($events); + $events->shouldReceive('dispatch')->once()->withArgs(function ($x) use ($connection) { + self::assertEquals($x, new TransactionBeginning($connection)); + + return true; + }); + + $connection->beginTransaction(); + } + + public function testRollBackedFiresEventsIfSet(): void + { + $connection = $this->getConnection(); + + $events = M::mock(Dispatcher::class); + $connection->setEventDispatcher($events); + $events->shouldReceive('dispatch')->once()->withArgs(function ($x) use ($connection) { + self::assertEquals($x, new TransactionRolledBack($connection)); + + return true; + }); + $connection->rollback(); + } + + public function testTransactionMethodRunsSuccessfully(): void + { + $connection = $this->getConnection(); + + $result = $connection->transaction(function ($db) { + return $db; + }); + $this->assertEquals($connection, $result); + } + + public function testTransactionMethodRollsbackAndThrows(): void + { + $connection = $this->getConnection(); + + try { + $connection->transaction(function () { + throw new RuntimeException('foo'); + }); + } catch (Throwable $e) { + $this->assertEquals('foo', $e->getMessage()); + } + } + + public function testFromCreatesNewQueryBuilder(): void + { + $builder = $this->getConnection()->table('User'); + + $this->assertInstanceOf(Builder::class, $builder); + $this->assertEquals('User', $builder->from); + } + + /* + * Utility methods below this line + */ + + public function createUser() + { + /** @var Connection $c */ + $c = $this->getConnection('default'); + + $create = 'CREATE (u:User {name: $name, email: $email, username: $username})'; + + return $c->getSession()->run($create, $this->user); + } +} diff --git a/tests/Functional/HasManyRelationTest.php b/tests/Functional/HasManyRelationTest.php new file mode 100644 index 00000000..d7e5dfd4 --- /dev/null +++ b/tests/Functional/HasManyRelationTest.php @@ -0,0 +1,139 @@ + 'George R. R. Martin']); + + $got = new Permission(['title' => 'A Game of Thrones', 'alias' => '704']); + $cok = new Permission(['title' => 'A Clash of Kings', 'alias' => '768']); + + $role->permissions()->save($got); + $role->permissions()->save($cok); + + $role = Role::first(); + $books = $role->permissions; + + $expectedBooks = [ + 'A Game of Thrones' => $got->getAttributes(), + 'A Clash of Kings' => $cok->getAttributes(), + ]; + + $this->assertCount(2, $books->toArray()); + + foreach ($books as $book) { + $this->assertEquals($expectedBooks[$book->title], $book->getAttributes()); + } + } + + public function testSavingManyAndDynamicLoading() + { + $author = Role::create(['title' => 'George R. R. Martin']); + + $novel = [ + new Permission([ + 'title' => 'A Game of Thrones', + 'alias' => '704', + ]), + new Permission([ + 'title' => 'A Clash of Kings', + 'alias' => '768', + ]), + new Permission([ + 'title' => 'A Storm of Swords', + 'alias' => '992', + ]), + new Permission([ + 'title' => 'A Feast for Crows', + 'alias' => '753', + ]), + ]; + + $edges = $author->permissions()->saveMany($novel); + $this->assertCount(count($novel), $edges); + + $books = $author->permissions->toArray(); + $this->assertCount(count($novel), $books); + } + + public function testCreatingSingleRelatedModels() + { + $author = Role::create(['title' => 'George R. R. Martin']); + + $novel = [ + [ + 'title' => 'A Game of Thrones', + 'alias' => '704', + ], + [ + 'title' => 'A Clash of Kings', + 'alias' => '768', + ], + [ + 'title' => 'A Storm of Swords', + 'alias' => '992', + ], + [ + 'title' => 'A Feast for Crows', + 'alias' => '753', + ], + ]; + + foreach ($novel as $book) { + $edge = $author->permissions()->create($book); + + $this->assertInstanceOf(Permission::class, $edge); + $this->assertNotNull($edge->created_at); + $this->assertNotNull($edge->updated_at); + } + } + + public function testEagerLoadingHasMany() + { + $author = Role::create(['title' => 'George R. R. Martin']); + + $novel = [ + new Permission([ + 'title' => 'A Game of Thrones', + 'alias' => '704', + ]), + new Permission([ + 'title' => 'A Clash of Kings', + 'alias' => '768', + ]), + new Permission([ + 'title' => 'A Storm of Swords', + 'alias' => '992', + ]), + new Permission([ + 'title' => 'A Feast for Crows', + 'alias' => '753', + ]), + ]; + + $edges = $author->permissions()->saveMany($novel); + $this->assertCount(count($novel), $edges); + + $author = Role::with('permissions')->find($author->getKey()); + $relations = $author->getRelations(); + + $this->assertArrayHasKey('permissions', $relations); + $this->assertCount(count($novel), $relations['permissions']); + + $booksIds = array_map(function ($book) { + return $book->getKey(); + }, $novel); + + $this->assertEquals(['704', '768', '992', '753'], $booksIds); + } +} diff --git a/tests/Functional/HasOneRelationTest.php b/tests/Functional/HasOneRelationTest.php new file mode 100644 index 00000000..6a6ac787 --- /dev/null +++ b/tests/Functional/HasOneRelationTest.php @@ -0,0 +1,66 @@ + 'Tests', 'email' => 'B']); + $profile = Profile::create(['guid' => uniqid(), 'service' => 'twitter']); + + $user->profile()->save($profile); + + $this->assertEquals($profile->toArray(), $user->profile->toArray()); + } + + public function testDynamicLoadingHasOneFromFoundRecord() + { + $user = User::create(['name' => 'Tests', 'email' => 'B']); + $profile = Profile::create(['guid' => uniqid(), 'service' => 'twitter']); + + $user->profile()->save($profile); + + $found = User::find($user->getKey()); + + $this->assertEquals($profile->toArray(), $found->profile->toArray()); + } + + public function testEagerLoadingHasOne() + { + $user = User::create(['name' => 'Tests', 'email' => 'B']); + $profile = Profile::create(['guid' => uniqid(), 'service' => 'twitter']); + + $relation = $user->profile()->save($profile); + + $found = User::with('profile')->find($user->getKey()); + $relations = $found->getRelations(); + + $this->assertInstanceOf(Profile::class, $relation); + $this->assertArrayHasKey('profile', $relations); + + $this->assertEquals($profile->toArray(), $user->profile->toArray()); + } + + public function testSavingMultipleRelationsKeepsOnlyTheLastOne() + { + $user = User::create(['name' => 'Tests', 'email' => 'B']); + $profile = new Profile(['guid' => uniqid(), 'service' => 'twitter']); + + $user->profile()->save($profile); + $user->refresh(); + $cv = new Profile(['guid' => uniqid(), 'service' => 'linkedin']); + $user->profile()->update([$user->profile()->getForeignKeyName() => null]); + + $user->profile()->save($cv); + $user->refresh(); + $this->assertEquals('linkedin', $user->profile->service); + } +} diff --git a/tests/Functional/OrdersAndLimitsTest.php b/tests/Functional/OrdersAndLimitsTest.php new file mode 100644 index 00000000..0fd1631d --- /dev/null +++ b/tests/Functional/OrdersAndLimitsTest.php @@ -0,0 +1,49 @@ + 'a']); + $c2 = Role::create(['title' => 'b']); + $c3 = Role::create(['title' => 'c']); + + $clicks = Role::orderBy('title', 'desc')->get(); + + $this->assertCount(3, $clicks); + + $this->assertEquals($c3->toArray(), $clicks[0]->toArray()); + $this->assertEquals($c2->toArray(), $clicks[1]->toArray()); + $this->assertEquals($c1->toArray(), $clicks[2]->toArray()); + + $asc = Role::orderBy('title', 'asc')->get(); + + $this->assertEquals($c1->toArray(), $asc[0]->toArray()); + $this->assertEquals($c2->toArray(), $asc[1]->toArray()); + $this->assertEquals($c3->toArray(), $asc[2]->toArray()); + } + + public function testFetchingLimitedOrderedRecords() + { + $c1 = Role::create(['title' => 'a']); + $c2 = Role::create(['title' => 'b']); + $c3 = Role::create(['title' => 'c']); + + $click = Role::orderBy('title', 'desc')->take(1)->get(); + $this->assertCount(1, $click); + $this->assertEquals($c3->title, $click[0]->title); + + $another = Role::orderBy('title', 'asc')->take(2)->get(); + $this->assertCount(2, $another); + $this->assertEquals($c1->title, $another[0]->title); + $this->assertEquals($c2->title, $another[1]->title); + } +} diff --git a/tests/Functional/ParameterGroupingTest.php b/tests/Functional/ParameterGroupingTest.php new file mode 100644 index 00000000..e9ab6fd2 --- /dev/null +++ b/tests/Functional/ParameterGroupingTest.php @@ -0,0 +1,43 @@ +getConnection()->affectingStatement('MATCH (x) DETACH DELETE x'); + } + + public function testNestedWhereClause() + { + $searchedUser = User::create(['name' => 'John Doe']); + $searchedUser->profile()->save( + Profile::create([ + 'guid' => 'abcd', + 'service' => 'Music', + ])); + + $anotherUser = User::create(['name' => 'John Smith']); + $anotherUser->profile()->save( + Profile::create([ + 'guid' => 'abc', + 'service' => 'Music', + ])); + + $users = User::whereHas('profile', function ($query) { + $query->where('guid', 'abc')->where(function ($query) { + $query->orWhere('service', 'Music')->orWhere('service', 'Video'); + }); + })->get(); + + $this->assertCount(1, $users); + $this->assertEquals('John Smith', $users->first()->name); + } +} diff --git a/tests/Functional/PolymorphicHyperMorphToTest.php b/tests/Functional/PolymorphicHyperMorphToTest.php new file mode 100644 index 00000000..d5dd8aa0 --- /dev/null +++ b/tests/Functional/PolymorphicHyperMorphToTest.php @@ -0,0 +1,38 @@ + 'Hmm...']); + User::create(['name' => 'I Comment On Posts']); + User::create(['name' => 'I Comment On Videos']); + // create the user's post and video + $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); // Grab them back + + $post = $user->posts->first(); + + $this->assertInstanceOf(Post::class, $post); + $this->assertEquals('Another Place', $post->getKey()); + } + + public function testAttachingNonExistingModelIds() + { + $user = User::create(['name' => 'Hmm...']); + $user->posts()->create(['title' => 'A little posty post.']); + $post = $user->posts()->first(); + + $this->expectException(ModelNotFoundException::class); + $user->posts()->findOrFail(9999999999); + } +} diff --git a/tests/Functional/QueryingRelationsTest.php b/tests/Functional/QueryingRelationsTest.php new file mode 100644 index 00000000..057ee98c --- /dev/null +++ b/tests/Functional/QueryingRelationsTest.php @@ -0,0 +1,174 @@ +getConnection()->statement('MATCH (n) DETACH DELETE n'); + } + + public function testQueryingNestedHas() + { + // user with a role that has only one permission + $user = User::create(['name' => 'cappuccino']); + $role = Role::create(['title' => 'pikachu']); + $permission = Permission::create(['title' => 'Elephant', 'alias' => 'elephant']); + $role->permissions()->save($permission); + $user->roles()->save($role); + + // user with a role that has 2 permissions + $userWithTwo = User::create(['name' => 'frappe']); + $roleWithTwo = Role::create(['title' => 'pikachuu']); + $permissionOne = Permission::create(['title' => 'Goomba', 'alias' => 'goomba']); + $permissionTwo = Permission::create(['title' => 'Boomba', 'alias' => 'boomba']); + $roleWithTwo->permissions()->saveMany([$permissionOne, $permissionTwo]); + $userWithTwo->roles()->save($roleWithTwo); + + // user with a role that has no permission + $user2 = User::Create(['name' => 'u2']); + $role2 = Role::create(['title' => 'nosperm']); + + $user2->roles()->save($role2); + + // get the users where their roles have at least one permission. + $found = User::has('roles.permissions')->get(); + + $this->assertCount(2, $found); + $this->assertInstanceOf(User::class, $found[1]); + $this->assertEquals($userWithTwo->toArray(), $found->where('name', 'frappe')->first()->toArray()); + $this->assertInstanceOf(User::class, $found[0]); + $this->assertEquals($user->toArray(), $found->where('name', 'cappuccino')->first()->toArray()); + + $moreThanOnePermission = User::has('roles.permissions', '>=', 2)->get(); + $this->assertCount(1, $moreThanOnePermission); + $this->assertInstanceOf( + User::class, + $moreThanOnePermission[0] + ); + $this->assertEquals($userWithTwo->toArray(), $moreThanOnePermission[0]->toArray()); + } + + public function testQueryingWhereHasOne() + { + $mrAdmin = User::create(['name' => 'Rundala']); + $anotherAdmin = User::create(['name' => 'Makhoul']); + $mrsEditor = User::create(['name' => 'Mr. Moonlight']); + $mrsManager = User::create(['name' => 'Batista']); + $anotherManager = User::create(['name' => 'Quin Tukee']); + + $admin = Role::create(['title' => 'admin']); + $editor = Role::create(['title' => 'editor']); + $manager = Role::create(['title' => 'manager']); + + $mrAdmin->roles()->save($admin); + $anotherAdmin->roles()->save($admin); + $mrsEditor->roles()->save($editor); + $mrsManager->roles()->save($manager); + $anotherManager->roles()->save($manager); + + // check admins + $admins = User::whereHas('roles', function ($q) { + $q->where('title', 'admin'); + })->get(); + $this->assertCount(2, $admins); + $expectedAdmins = [$mrAdmin->getKey(), $anotherAdmin->getKey()]; + $this->assertEqualsCanonicalizing($expectedAdmins, $admins->pluck($mrAdmin->getKeyName())->toArray()); + + // check editors + $editors = User::whereHas('roles', function ($q) { + $q->where('title', 'editor'); + })->get(); + $this->assertCount(1, $editors); + $this->assertEquals($mrsEditor->toArray(), $editors->first()->toArray()); + + // check managers + $expectedManagers = [$mrsManager->getKey(), $anotherManager->getKey()]; + $managers = User::whereHas('roles', function ($q) { + $q->where('title', 'manager'); + })->get(); + $this->assertCount(2, $managers); + $this->assertEqualsCanonicalizing( + $expectedManagers, + $managers->pluck($anotherManager->getKeyName())->toArray() + ); + } + + public function testQueryingWhereHasById() + { + $user = User::create(['name' => 'cappuccino']); + $role = Role::create(['title' => 'pikachu']); + + $user->roles()->save($role); + + $found = User::whereHas('roles', function ($q) use ($role) { + $q->where('title', $role->getKey()); + })->first(); + + $this->assertInstanceOf(User::class, $found); + } + + public function testQueryingNestedWhereHasUsingProperty() + { + // user with a role that has only one permission + $user = User::create(['name' => 'cappuccino']); + $role = Role::create(['title' => 'pikachu']); + $permission = Permission::create(['title' => 'Elephant', 'alias' => 'elephant']); + $role->permissions()->save($permission); + $user->roles()->save($role); + + // user with a role that has 2 permissions + $userWithTwo = User::create(['name' => 'cappuccino0']); + $roleWithTwo = Role::create(['title' => 'pikachuU']); + $permissionOne = Permission::create(['title' => 'Goomba', 'alias' => 'goomba']); + $permissionTwo = Permission::create(['title' => 'Boomba', 'alias' => 'boomba']); + $roleWithTwo->permissions()->saveMany([$permissionOne, $permissionTwo]); + $userWithTwo->roles()->save($roleWithTwo); + + $found = User::whereHas('roles', function ($q) use ($role, $permission) { + $q->where('title', $role->title); + $q->whereHas('permissions', function ($q) use ($permission) { + $q->where('title', $permission->title); + }); + })->get(); + + $this->assertCount(1, $found); + $this->assertInstanceOf(User::class, $found->first()); + $this->assertEquals($user->toArray(), $found->first()->toArray()); + } + + public function testSavingRelationWithDateTimeAndCarbonInstances() + { + $user = User::create(['name' => 'Andrew Hale']); + $yesterday = Carbon::now(); + $brother = new User(['name' => 'Simon Hale', 'dob' => $yesterday]); + + $dt = new DateTime(); + $someone = User::create(['name' => 'Producer', 'dob' => $dt]); + + $user->colleagues()->save($someone); + $user->colleagues()->save($brother); + + $andrew = User::find('Andrew Hale'); + + $colleagues = $andrew->colleagues()->get(); + $this->assertEquals( + $dt->format($andrew->getDateFormat()), + $colleagues[0]->dob->format($andrew->getDateFormat()) + ); + $this->assertEquals( + $yesterday->format($andrew->getDateFormat()), + $colleagues[1]->dob->format($andrew->getDateFormat()) + ); + } +} diff --git a/tests/Functional/ReflectionTests.php b/tests/Functional/ReflectionTests.php new file mode 100644 index 00000000..47a2cd23 --- /dev/null +++ b/tests/Functional/ReflectionTests.php @@ -0,0 +1,115 @@ + $class + * @param list $skippingMethods + */ + public function testImplementation(string $class, array $skippingMethods): void + { + $reflection = new ReflectionClass($class); + foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + // These are approved methods that do not require their behaviour to be overridden. + if (in_array($method->getName(), $skippingMethods)) { + continue; + } + + // We try to guard against blind spots if a new update arrives and a public method appears that we have not overridden and did not account for. + self::assertEquals( + $class, + $method->getDeclaringClass()->getName(), + sprintf('Method %s::%s is not overridden', $class, $method->getName()) + ); + } + } + + public static function classesAndSkippingMethods(): array + { + return [ + CypherGrammar::class => [ + CypherGrammar::class, + [ + 'setConnection', + 'macro', + 'mixin', + 'hasMacro', + 'flushMacros', + '__callStatic', + '__call', + ], + ], + Connection::class => [ + Connection::class, + [ + 'useDefaultQueryGrammar', + 'useDefaultSchemaGrammar', + 'useDefaultPostProcessor', + 'table', + 'selectFromWriteConnection', + 'update', + 'delete', + 'pretend', + 'logQuery', + 'whenQueryingForLongerThan', + 'allowQueryDurationHandlersToRunAgain', + 'totalQueryDuration', + 'resetTotalQueryDuration', + 'beforeExecuting', + 'listen', + 'raw', + 'useWriteConnectionWhenReading', + 'setPdo', + 'setReadPdo', + 'setReconnector', + 'getName', + 'getNameWithReadWriteType', + 'getConfig', + 'getDriverName', + 'getQueryGrammar', + 'setQueryGrammar', + 'getSchemaGrammar', + 'setSchemaGrammar', + 'getPostProcessor', + 'setPostProcessor', + 'getEventDispatcher', + 'setEventDispatcher', + 'unsetEventDispatcher', + 'setTransactionManager', + 'unsetTransactionManager', + 'pretending', + 'getQueryLog', + 'flushQueryLog', + 'enableQueryLog', + 'disableQueryLog', + 'logging', + 'getDatabaseName', + 'setDatabaseName', + 'setReadWriteType', + 'getTablePrefix', + 'setTablePrefix', + 'withTablePrefix', + 'resolverFor', + 'getResolver', + 'afterCommit', + 'macro', + 'mixin', + 'hasMacro', + 'flushMacros', + '__callStatic', + '__call', + ], + ], + ]; + } +} diff --git a/tests/Functional/RelatedTest.php b/tests/Functional/RelatedTest.php new file mode 100644 index 00000000..17b7ee89 --- /dev/null +++ b/tests/Functional/RelatedTest.php @@ -0,0 +1,66 @@ +getConnection()->statement('MATCH (n) DETACH DELETE n'); + } + + public function testSimple(): void + { + $user = User::create(['name' => 'User']); + + $user->relatedRoles()->create([ + 'title' => 'Role', + ]); + + $results = $this->getConnection()->select('RETURN COUNT { MATCH (u:Individual) - [:HAS_ROLE] -> (r:Role) } AS count'); + + $this->assertEquals(1, $results[0]['count']); + } + + public function testMultiple(): void + { + $user = User::create(['name' => 'User']); + + $user->relatedRoles()->createMany([ + ['title' => 'Role'], + ['title' => 'Bowling'], + ['title' => 'Test'], + ]); + + $results = $this->getConnection()->select('RETURN COUNT { MATCH (u:Individual) - [:HAS_ROLE] -> (r:Role) } AS count'); + $this->assertEquals(3, $results[0]['count']); + + $results = $this->getConnection()->select('RETURN COUNT { MATCH (u:Individual) } AS count'); + $this->assertEquals(1, $results[0]['count']); + + $this->assertEquals(3, $user->relatedRoles->count()); + } + + public function testSync(): void + { + $user = User::create(['name' => 'User']); + + $role1 = Role::create(['title' => 'Role']); + Role::create(['title' => 'Bowling']); + $role3 = Role::create(['title' => 'Test']); + + $user->relatedRoles()->sync([$role1->getKey(), $role3->getKey()]); + + $results = $this->getConnection()->select('RETURN COUNT { MATCH (u:Individual) - [:HAS_ROLE] -> (r:Role) } AS count'); + $this->assertEquals(2, $results[0]['count']); + + $results = $this->getConnection()->select('RETURN COUNT { MATCH (u:Individual) } AS count'); + $this->assertEquals(1, $results[0]['count']); + } +} diff --git a/tests/Functional/RelationshipJoinTests.php b/tests/Functional/RelationshipJoinTests.php new file mode 100644 index 00000000..10237bbe --- /dev/null +++ b/tests/Functional/RelationshipJoinTests.php @@ -0,0 +1,53 @@ +insert([ + ['id' => 1], + ['id' => 2], + ['id' => 3], + ['id' => 4], + ]); + + Builder::from('Alias') + ->insert([ + ['name' => 'admin'], + ['name' => 'teacher'], + ['name' => 'student'], + ]); + + Builder::from('User') + ->whereIn('id', [2, 3]) + ->join('HAS_ALIAS>', function (JoinClause $clause) { + $clause->joinWhere('Alias', 'name', '=', 'admin'); + }) + ->insert([ + 'HAS_ALIAS' => ['id' => 1], + ]); + + Builder::from('leftJoinWhere('Alias', 'name', '=', 'teacher') + ->rightJoinWhere('User', 'id', '=', 1) + ->insert([ + ['id' => 2], + ]); + + $paths = Builder::from('x') + ->getConnection() + ->select('MATCH (x:User) - [r:HAS_ALIAS] -> (y:Alias) RETURN x, r, y'); + + $this->assertCount(3, $paths); + } +} diff --git a/tests/Functional/SchemaTest.php b/tests/Functional/SchemaTest.php new file mode 100644 index 00000000..eb45aa7c --- /dev/null +++ b/tests/Functional/SchemaTest.php @@ -0,0 +1,25 @@ +getConnection()->affectingStatement('MATCH (x) DETACH DELETE x'); + } + + public function testHasColumn(): void + { + $this->assertFalse(Schema::hasColumn('User', 'email')); + + $this->getConnection()->affectingStatement('CREATE (:User {email: "test@test"})'); + + $this->assertTrue(Schema::hasColumn('User', 'email')); + } +} \ No newline at end of file diff --git a/tests/Functional/WheresTheTest.php b/tests/Functional/WheresTheTest.php new file mode 100644 index 00000000..718cfee1 --- /dev/null +++ b/tests/Functional/WheresTheTest.php @@ -0,0 +1,308 @@ +getConnection()->affectingStatement('MATCH (n) DETACH DELETE n'); + + // Setup the data in the database + $this->ab = User::create([ + 'name' => 'Ey Bee', + 'alias' => 'ab', + 'email' => 'ab@alpha.bet', + 'calls' => 10, + ]); + + $this->cd = User::create([ + 'name' => 'See Dee', + 'alias' => 'cd', + 'email' => 'cd@alpha.bet', + 'calls' => 20, + ]); + + $this->ef = User::create([ + 'name' => 'Eee Eff', + 'alias' => 'ef', + 'email' => 'ef@alpha.bet', + 'calls' => 30, + ]); + + $this->gh = User::create([ + 'name' => 'Gee Aych', + 'alias' => 'gh', + 'email' => 'gh@alpha.bet', + 'calls' => 40, + ]); + + $this->ij = User::create([ + 'name' => 'Eye Jay', + 'alias' => 'ij', + 'email' => 'ij@alpha.bet', + 'calls' => 50, + ]); + } + + public function testWhereIdWithNoOperator() + { + $u = User::where('name', $this->ab->getKey())->first(); + + $this->assertEquals($this->ab->toArray(), $u->toArray()); + } + + public function testWhereIdSelectingProperties() + { + $u = User::where('name', $this->ab->getKey())->first(['name', 'email']); + + $this->assertEquals($this->ab->getKey(), $u->getKey()); + $this->assertEquals($this->ab->name, $u->name); + $this->assertEquals($this->ab->email, $u->email); + } + + public function testWhereIdWithEqualsOperator() + { + $u = User::where('name', '=', $this->cd->getKey())->first(); + + $this->assertEquals($this->cd->toArray(), $u->toArray()); + } + + public function testWherePropertyWithoutOperator() + { + $u = User::where('alias', 'ab')->first(); + + $this->assertEquals($this->ab->toArray(), $u->toArray()); + } + + public function testWherePropertyEqualsOperator() + { + $u = User::where('alias', '=', 'ab')->first(); + + $this->assertEquals($this->ab->toArray(), $u->toArray()); + } + + public function testWhereGreaterThanOperator() + { + $u = User::where('calls', '>', 10)->orderBy('calls')->first(); + $this->assertEquals($this->cd->toArray(), $u->toArray()); + + $others = User::where('calls', '>', 10)->orderBy('calls')->get(); + $this->assertCount(4, $others); + + $brothers = [ + $this->cd->toArray(), + $this->ef->toArray(), + $this->gh->toArray(), + $this->ij->toArray(), + ]; + $this->assertEquals($brothers, $others->toArray()); + + $lastTwo = User::where('calls', '>=', 40)->orderBy('calls')->get(); + $this->assertCount(2, $lastTwo); + + $mothers = [$this->gh->toArray(), $this->ij->toArray()]; + $this->assertEquals($mothers, $lastTwo->toArray()); + + $none = User::where('calls', '>', 9000)->get(); + $this->assertCount(0, $none); + } + + public function testWhereLessThanOperator() + { + $u = User::where('calls', '<', 10)->get(); + $this->assertCount(0, $u); + + $ab = User::where('calls', '<', 20)->orderBy('calls')->first(); + $this->assertEquals($this->ab->toArray(), $ab->toArray()); + + $three = User::where('calls', '<=', 30)->orderBy('calls')->get(); + $this->assertCount(3, $three); + + $cocoa = [ + $this->ab->toArray(), + $this->cd->toArray(), + $this->ef->toArray(), + ]; + $this->assertEquals($cocoa, $three->sortBy('alias')->toArray()); + + $below = User::where('calls', '<', -100)->get(); + $this->assertCount(0, $below); + + $nil = User::where('calls', '<=', 0)->first(); + $this->assertNull($nil); + } + + public function testWhereDifferentThanOperator() + { + $notab = User::where('alias', '<>', 'ab')->orderBy('alias')->get(); + + $dudes = [ + $this->cd->toArray(), + $this->ef->toArray(), + $this->gh->toArray(), + $this->ij->toArray(), + ]; + + $this->assertCount(4, $notab); + $this->assertEquals($dudes, $notab->toArray()); + } + + public function testWhereIn() + { + $alpha = User::whereIn('alias', ['ab', 'cd', 'ef', 'gh', 'ij'])->orderBy('alias')->get(); + + $crocodile = collect([ + $this->ab->toArray(), + $this->cd->toArray(), + $this->ef->toArray(), + $this->gh->toArray(), + $this->ij->toArray(), + ])->sortBy('alias')->toArray(); + + $this->assertEquals($crocodile, $alpha->sortBy('alias')->toArray()); + } + + public function testWhereNotNull() + { + $alpha = User::whereNotNull('alias')->orderBy('calls')->get(); + + $crocodile = collect([ + $this->ab->toArray(), + $this->cd->toArray(), + $this->ef->toArray(), + $this->gh->toArray(), + $this->ij->toArray(), + ])->sortBy('alias')->toArray(); + + $this->assertEquals($crocodile, $alpha->sortBy('alias')->toArray()); + } + + public function testWhereNull() + { + $u = User::whereNull('calls')->get(); + $this->assertCount(0, $u); + } + + public function testWhereNotIn() + { + /* + * There is no WHERE NOT IN [ids] in Neo4j, it should be something like this: + * + * MATCH (actor:Actor {name:"Tom Hanks"} )-[:ACTED_IN]->(movies)<-[:ACTED_IN]-(coactor) + * WITH collect(distinct coactor) as coactors + * MATCH (actor:Actor) + * WHERE actor NOT IN coactors + * RETURN actor + */ + $u = User::whereNotIn('alias', ['ab', 'cd', 'ef'])->orderBy('calls')->get(); + $still = [$this->gh->toArray(), $this->ij->toArray()]; + + $this->assertCount(2, $u); + $this->assertEquals($still, $u->toArray()); + } + + public function testOrWhere() + { + $buddies = User::where('name', 'Ey Bee') + ->orWhere('alias', 'cd') + ->orWhere('email', 'ef@alpha.bet') + ->orWhere('name', $this->gh->getKey()) + ->orWhere('calls', '>', 40) + ->orderBy('calls') + ->get(); + + $this->assertCount(5, $buddies); + $bigBrothers = [ + $this->ab->toArray(), + $this->cd->toArray(), + $this->ef->toArray(), + $this->gh->toArray(), + $this->ij->toArray(), + ]; + + $this->assertEquals($bigBrothers, $buddies->toArray()); + } + + public function testOrWhereIn() + { + $all = User::whereIn('name', [$this->ab->getKey(), $this->cd->getKey()]) + ->orWhereIn('alias', ['ef', 'gh', 'ij']) + ->get(); + + $padrougas = [ + $this->ab->toArray(), + $this->cd->toArray(), + $this->ef->toArray(), + $this->gh->toArray(), + $this->ij->toArray(), + ]; + + $array = $all->toArray(); + usort($array, static fn (array $x, array $y) => $x['name'] <=> $y['name']); + + $padrougasArray = $padrougas; + usort($padrougasArray, static fn (array $x, array $y) => $x['name'] <=> $y['name']); + + $this->assertEquals($padrougasArray, $array); + } + + public function testWhereNotFound() + { + $u = User::where('name', '<', 1)->get(); + $this->assertCount(0, $u); + + $u2 = User::where('glasses', 'always on')->first(); + $this->assertNull($u2); + } + + /** + * Regression test for issue #19. + * + * @see https://github.com/Vinelab/NeoEloquent/issues/19 + */ + public function testWhereMultipleValuesForSameColumn() + { + $u = User::where('alias', '=', 'ab') + ->orWhere('alias', '=', 'cd') + ->orderBy('alias') + ->get(); + + $this->assertCount(2, $u); + $this->assertEquals('ab', $u[0]->alias); + $this->assertEquals('cd', $u[1]->alias); + } + + /** + * Regression test for issue #41. + * + * @see https://github.com/Vinelab/NeoEloquent/issues/41 + */ + public function testWhereWithIn() + { + $ab = User::whereIn('alias', ['ab'])->first(); + + $this->assertEquals($this->ab->toArray(), $ab->toArray()); + + $users = User::whereIn('alias', ['cd', 'ef'])->orderBy('alias')->get(); + + $this->assertEquals($this->cd->toArray(), $users[0]->toArray()); + $this->assertEquals($this->ef->toArray(), $users[1]->toArray()); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index b241fae5..af7f6022 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,113 +2,56 @@ namespace Vinelab\NeoEloquent\Tests; -use Laudis\Neo4j\Contracts\ClientInterface; -use Laudis\Neo4j\Databags\SummarizedResult; -use Mockery as M; -use Vinelab\NeoEloquent\Connection; -use PHPUnit\Framework\TestCase as PHPUnit; -use Vinelab\NeoEloquent\Eloquent\Model; +use Illuminate\Contracts\Config\Repository; +use Orchestra\Testbench\TestCase as BaseTestCase; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; +use Vinelab\NeoEloquent\NeoEloquentServiceProvider; -class Stub extends Model +class TestCase extends BaseTestCase { -} - -class TestCase extends PHPUnit -{ - public function __construct() - { - parent::__construct(); - - // load custom configuration file - $this->dbConfig = require 'config/database.php'; - } - - public function setUp(): void - { - parent::setUp(); - - $resolver = M::mock('Illuminate\Database\ConnectionResolverInterface'); - $resolver->shouldReceive('connection')->andReturn($this->getConnectionWithConfig('default')); - - Stub::setConnectionResolver($resolver); - $this->flushDb(); - } - - public function tearDown(): void - { - // everything should be clean before every test - $this->flushDb(); - - parent::tearDown(); - } - - public static function setUpBeforeClass(): void - { - date_default_timezone_set('Asia/Beirut'); - } - - /** - * Get the connection with a given or the default configuration. - * - * @param string $config As specified in config/database.php - * - * @return \Vinelab\NeoEloquent\Connection - */ - protected function getConnectionWithConfig($config = null) - { - $connection = is_null($config) ? $this->dbConfig['connections']['default'] : - $this->dbConfig['connections'][$config]; - - return new Connection($connection); - } - - /** - * Flush all database records. - */ - protected function flushDb() - { - /** @var ClientInterface $client */ - $client = $this->getClient(); - - $flushQuery = 'MATCH (n) DETACH DELETE n'; - - $client->run($flushQuery); - } - - protected function getClient() - { - $connection = (new Stub())->getConnection(); - - return $connection->getClient(); - } - - /** - * get the node by the given id. - * - * @param int $id - * - * @return \Neoxygen\NeoClient\Formatter\Node - */ - protected function getNodeById($id) + protected function getPackageProviders($app): array { - //get the labels using NeoClient - $connection = $this->getConnectionWithConfig('neo4j'); - $client = $connection->getClient(); - /** @var SummarizedResult $result */ - $result = $client->run("MATCH (n) WHERE id(n)=$id RETURN n"); - - return $result->first()->first()->getValue(); + return [ + NeoEloquentServiceProvider::class, + ]; } /** - * Get node labels of a node by the given id. - * - * @param int $id - * - * @return array + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - protected function getNodeLabels($id) - { - return $this->getNodeById($id)->labels()->toArray(); + protected function getEnvironmentSetUp($app): void + { + parent::getEnvironmentSetUp($app); + /** @var Repository $config */ + $config = $app->get('config'); + $config->set('database.default', 'neo4j'); + + $connections = $config->get('database.connections'); + $connections = array_merge($connections, [ + 'default' => [ + 'driver' => 'neo4j', + 'host' => env('NEO4J_HOST', 'localhost'), + 'database' => env('NEO4J_DATABASE', 'neo4j'), + 'port' => env('NEO4J_PORT', 7687), + 'username' => env('NEO4J_USER', 'neo4j'), + 'password' => env('NEO4J_PASSWORD', 'testtest'), + ], + 'neo4j' => [ + 'driver' => 'neo4j', + 'host' => env('NEO4J_HOST', 'localhost'), + 'database' => env('NEO4J_DATABASE', 'neo4j'), + 'port' => env('NEO4J_PORT', 7687), + 'username' => env('NEO4J_USER', 'neo4j'), + 'password' => env('NEO4J_PASSWORD', 'testtest'), + ], + ]); + $config->set('database.connections', $connections); + } + + public function getAnnotations(): array + { + return []; } } diff --git a/tests/Vinelab/NeoEloquent/ConnectionFactoryTest.php b/tests/Vinelab/NeoEloquent/ConnectionFactoryTest.php deleted file mode 100644 index d2f18208..00000000 --- a/tests/Vinelab/NeoEloquent/ConnectionFactoryTest.php +++ /dev/null @@ -1,104 +0,0 @@ -factory = new ConnectionFactory(new Container()); - } - - public function tearDown(): void - { - } - - public function testSingleConnection() - { - $config = [ - 'type' => 'single', - 'host' => 'server.host', - 'port' => 7474, - 'username' => 'theuser', - 'password' => 'thepass', - ]; - - $connection = $this->factory->make($config); - $client = $connection->getClient(); - - $this->assertInstanceOf(Connection::class, $connection); - $this->assertInstanceOf(ClientInterface::class, $client); - - $this->assertEquals($config, $connection->getConfig()); - } - - public function testMultipleConnections() - { - $config = [ - - 'default' => 'server1', - - 'connections' => [ - - 'server1' => [ - 'host' => 'server1.host', - 'username' => 'theuser', - 'password' => 'thepass', - ], - - 'server2' => [ - 'host' => 'server2.host', - 'username' => 'anotheruser', - 'password' => 'anotherpass', - ], - - ], - - ]; - - $connection = $this->factory->make($config); - - $this->assertInstanceOf(Connection::class, $connection); - $this->assertInstanceOf(ClientInterface::class, $connection->getClient()); - } - - public function testHAConnection() - { - $config = [ - 'replication' => true, - - 'connections' => [ - - 'master' => [ - 'host' => 'server1.ip.address', - 'username' => 'theuser', - 'password' => 'dapass', - ], - - 'slaves' => [ - 'slave-1' => [ - 'host' => 'server2.ip.address', - 'username' => 'anotheruser', - 'password' => 'somepass', - ], - 'slave-2' => [ - 'host' => 'server3.ip.address', - 'username' => 'moreusers', - 'password' => 'differentpass', - ], - ], - - ], - ]; - - $this->expectException(Exception::class); - $this->expectErrorMessage('High Availability mode is not supported anymore. Please use the neo4j scheme instead'); - $this->factory->make($config); - } -} diff --git a/tests/Vinelab/NeoEloquent/ConnectionTest.php b/tests/Vinelab/NeoEloquent/ConnectionTest.php deleted file mode 100644 index e66c0c86..00000000 --- a/tests/Vinelab/NeoEloquent/ConnectionTest.php +++ /dev/null @@ -1,526 +0,0 @@ -user = array( - 'name' => 'Mulkave', - 'email' => 'me@mulkave.io', - 'username' => 'mulkave', - ); - - $this->client = $this->getClient(); - } - - public function tearDown(): void - { - M::close(); - - parent::tearDown(); - } - - public function testConnection() - { - $c = $this->getConnectionWithConfig('neo4j'); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Connection', $c); - } - - public function testConnectionClientInstance() - { - $c = $this->getConnectionWithConfig('neo4j'); - - $client = $c->getClient(); - - $this->assertInstanceOf(ClientInterface::class, $client); - } - - public function testGettingConfigParam() - { - $c = $this->getConnectionWithConfig('neo4j'); - - $config = require(__DIR__.'/../../config/database.php'); - $this->assertEquals($c->getConfigOption('port'), $config['connections']['neo4j']['port']); - $this->assertEquals($c->getConfigOption('host'), $config['connections']['neo4j']['host']); - } - - public function testDriverName() - { - $c = $this->getConnectionWithConfig('neo4j'); - - $this->assertEquals('neo4j', $c->getDriverName()); - } - - public function testGettingClient() - { - $c = $this->getConnectionWithConfig('neo4j'); - - $this->assertInstanceOf(ClientInterface::class, $c->getClient()); - } - - public function testGettingDefaultHost() - { - $c = $this->getConnectionWithConfig('default'); - - $this->assertEquals('localhost', $c->getHost([])); - $this->assertEquals(7687, $c->getPort([])); - } - - public function testGettingDefaultPort() - { - $c = $this->getConnectionWithConfig('default'); - - $port = $c->getPort([]); - - $this->assertEquals(7687, $port); - $this->assertIsInt($port); - } - - public function testGettingQueryCypherGrammar() - { - $c = $this->getConnectionWithConfig('default'); - - $grammar = $c->getQueryGrammar(); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Query\Grammars\CypherGrammar', $grammar); - } - - public function testPrepareBindings() - { - $date = M::mock('DateTime'); - $date->shouldReceive('format')->once()->with('foo')->andReturn('bar'); - - $bindings = array('test' => $date); - - $conn = $this->getMockConnection(); - $grammar = M::mock('Vinelab\NeoEloquent\Query\Grammars\CypherGrammar'); - $grammar->shouldReceive('getDateFormat')->once()->andReturn('foo'); - $conn->setQueryGrammar($grammar); - $result = $conn->prepareBindings($bindings); - - $this->assertEquals(array('test' => 'bar'), $result); - } - - public function testLogQueryFiresEventsIfSet() - { - $connection = $this->getMockConnection(); - $connection->logQuery('foo', array(), time()); - $connection->setEventDispatcher($events = M::mock('Illuminate\Contracts\Events\Dispatcher')); - $events->shouldReceive('dispatch')->once()->with('illuminate.query', array('foo', array(), null, null)); - $connection->logQuery('foo', array(), null); - - self::assertTrue(true); - } - - public function testPretendOnlyLogsQueries() - { - $connection = $this->getMockConnection(); - $connection->enableQueryLog(); - $queries = $connection->pretend(function ($connection) { - $connection->select('foo bar', array('baz')); - }); - $this->assertEquals('foo bar', $queries[0]['query']); - $this->assertEquals(array('baz'), $queries[0]['bindings']); - } - - public function testPreparingSimpleBindings() - { - $bindings = array( - 'username' => 'jd', - 'name' => 'John Doe', - ); - - $c = $this->getConnectionWithConfig('default'); - - $prepared = $c->prepareBindings($bindings); - - $this->assertEquals($bindings, $prepared); - } - - public function testPreparingWheresBindings() - { - $bindings = array( - 'username' => 'jd', - 'email' => 'marie@curie.sci', - ); - - $c = $this->getConnectionWithConfig('default'); - - $expected = array( - 'username' => 'jd', - 'email' => 'marie@curie.sci', - ); - - $prepared = $c->prepareBindings($bindings); - - $this->assertEquals($expected, $prepared); - } - - public function testPreparingFindByIdBindings() - { - $bindings = array( - 'id' => 6, - ); - - $c = $this->getConnectionWithConfig('default'); - - $expected = array('idn' => 6); - - $prepared = $c->prepareBindings($bindings); - - $this->assertEquals($expected, $prepared); - } - - public function testPreparingWhereInBindings() - { - $bindings = array( - 'mc' => 'mc', - 'ae' => 'ae', - 'animals' => 'animals', - 'mulkave' => 'mulkave', - ); - - $c = $this->getConnectionWithConfig('default'); - - $expected = array( - 'mc' => 'mc', - 'ae' => 'ae', - 'animals' => 'animals', - 'mulkave' => 'mulkave', - ); - - $prepared = $c->prepareBindings($bindings); - - $this->assertEquals($expected, $prepared); - } - - public function testGettingCypherGrammar() - { - $c = $this->getConnectionWithConfig('default'); - - $cypher = 'MATCH (u:`User`) RETURN * LIMIT 10'; - $query = $c->getCypherQuery($cypher, array()); - - $this->assertIsArray($query); - $this->assertArrayHasKey('statement', $query); - $this->assertArrayHasKey('parameters', $query); - $this->assertEquals($cypher, $query['statement']); - } - - public function testCheckingIfBindingIsABinding() - { - $c = $this->getConnectionWithConfig('default'); - - $empty = array(); - $valid = array('key' => 'value'); - $invalid = array(array('key' => 'value')); - $bastard = array(array('key' => 'value'), 'another' => 'value'); - - $this->assertFalse($c->isBinding($empty)); - $this->assertFalse($c->isBinding($invalid)); - $this->assertFalse($c->isBinding($bastard)); - $this->assertTrue($c->isBinding($valid)); - } - - public function testCreatingConnection() - { - $c = $this->getConnectionWithConfig('default'); - - $connection = $c->createConnection(); - - $this->assertInstanceOf(ClientInterface::class, $connection); - } - - public function testSelectWithBindings() - { - $created = $this->createUser(); - - $query = 'MATCH (n:`User`) WHERE n.username = $username RETURN * LIMIT 1'; - - $bindings = ['username' => $this->user['username']]; - - $c = $this->getConnectionWithConfig('default'); - - $c->enableQueryLog(); - $results = $c->select($query, $bindings); - - $log = $c->getQueryLog(); - $log = reset($log); - - $this->assertEquals($log['query'], $query); - $this->assertEquals($log['bindings'], $bindings); - $this->assertInstanceOf(CypherList::class, $results); - - // This is how we get the first row of the result (first [0]) - // and then we get the Node instance (the 2nd [0]) - // and then ask it to return its properties - $selected = $results->first()->first()->getValue()->getProperties()->toArray(); - - $this->assertEquals($this->user, $selected, 'The fetched User must be the same as the one we just created'); - } - - /** - * @depends testSelectWithBindings - */ - public function testSelectWithBindingsById() - { - // Create the User record - $created = $this->createUser(); - - $c = $this->getConnectionWithConfig('default'); - $c->enableQueryLog(); - - $query = 'MATCH (n:`User`) WHERE n.username = $username RETURN * LIMIT 1'; - - // Get the ID of the created record - $results = $c->select($query, array('username' => $this->user['username'])); - - $node = $results->first()->first()->getValue(); - $id = $node->getId(); - - $bindings = array( - 'id' => $id, - ); - - // Select the Node containing the User record by its id - $query = 'MATCH (n:`User`) WHERE id(n) = $idn RETURN * LIMIT 1'; - - $results = $c->select($query, $bindings); - - $log = $c->getQueryLog(); - - $this->assertEquals($log[1]['query'], $query); - $this->assertEquals($log[1]['bindings'], $bindings); - $this->assertInstanceOf(CypherList::class, $results); - - $selected = $results->first()->first()->getValue()->getProperties()->toArray(); - - $this->assertEquals($this->user, $selected); - } - - public function testAffectingStatement() - { - $c = $this->getConnectionWithConfig('default'); - - $created = $this->createUser(); - - $type = 'dev'; - - // Now we update the type and set it to $type - $query = 'MATCH (n:`User`) WHERE n.username = $username '. - 'SET n.type = $type, n.updated_at = $updated_at '. - 'RETURN count(n)'; - - $bindings = array( - 'type' => $type, - 'updated_at' => '2014-05-11 13:37:15', - 'username' => $this->user['username'], - ); - - $results = $c->affectingStatement($query, $bindings); - - $this->assertInstanceOf(SummarizedResult::class, $results); - - /** @var CypherMap $result */ - foreach ($results as $result) { - $count = $result->first()->getValue(); - $this->assertEquals(1, $count); - } - - // Try to find the updated one and make sure it was updated successfully - $query = 'MATCH (n:User) WHERE n.username = $username RETURN n'; - $cypher = $c->getCypherQuery($query, array('username' => $this->user['username'])); - - $results = $this->client->run($cypher['statement'], $cypher['parameters']); - - $this->assertInstanceOf(CypherList::class, $results); - - $user = $results->first()->first()->getValue()->getProperties()->toArray(); - - $this->assertEquals($type, $user['type']); - } - - public function testAffectingStatementOnNonExistingRecord() - { - $c = $this->getConnectionWithConfig('default'); - - $type = 'dev'; - - // Now we update the type and set it to $type - $query = 'MATCH (n:`User`) WHERE n.username = $username '. - 'SET n.type = $type, n.updated_at = $updated_at '. - 'RETURN count(n)'; - - $bindings = array( - array('type' => $type), - array('updated_at' => '2014-05-11 13:37:15'), - array('username' => $this->user['username']), - ); - - $results = $c->affectingStatement($query, $bindings); - - $this->assertInstanceOf(SummarizedResult::class, $results); - - /** @var CypherMap $result */ - foreach ($results as $result) { - $count = $result->first()->getValue(); - $this->assertEquals(0, $count); - } - } - - public function testSettingDefaultCallsGetDefaultGrammar() - { - $connection = $this->getMockConnection(); - $mock = M::mock('StdClass'); - $connection->expects($this->once())->method('getDefaultQueryGrammar')->will($this->returnValue($mock)); - $connection->useDefaultQueryGrammar(); - $this->assertEquals($mock, $connection->getQueryGrammar()); - } - - public function testSettingDefaultCallsGetDefaultPostProcessor() - { - $connection = $this->getMockConnection(); - $mock = M::mock('StdClass'); - $connection->expects($this->once())->method('getDefaultPostProcessor')->will($this->returnValue($mock)); - $connection->useDefaultPostProcessor(); - $this->assertEquals($mock, $connection->getPostProcessor()); - } - - public function testSelectOneCallsSelectAndReturnsSingleResult() - { - $connection = $this->getMockConnection(array('select')); - $connection->expects($this->once())->method('select')->with('foo', array('bar' => 'baz'))->will($this->returnValue(array('foo'))); - $this->assertEquals('foo', $connection->selectOne('foo', array('bar' => 'baz'))); - } - - public function testInsertCallsTheStatementMethod() - { - $connection = $this->getMockConnection(array('statement')); - $connection->expects($this->once())->method('statement') - ->with($this->equalTo('foo'), $this->equalTo(array('bar'))) - ->will($this->returnValue('baz')); - $results = $connection->insert('foo', array('bar')); - $this->assertEquals('baz', $results); - } - - public function testUpdateCallsTheAffectingStatementMethod() - { - $connection = $this->getMockConnection(array('affectingStatement')); - $connection->expects($this->once())->method('affectingStatement')->with($this->equalTo('foo'), $this->equalTo(array('bar')))->will($this->returnValue('baz')); - $results = $connection->update('foo', array('bar')); - $this->assertEquals('baz', $results); - } - - public function testDeleteCallsTheAffectingStatementMethod() - { - $connection = $this->getMockConnection(array('affectingStatement')); - $connection->expects($this->once())->method('affectingStatement')->with($this->equalTo('foo'), $this->equalTo(array('bar')))->will($this->returnValue('baz')); - $results = $connection->delete('foo', array('bar')); - $this->assertEquals('baz', $results); - } - - public function testBeganTransactionFiresEventsIfSet() - { - $connection = $this->getMockConnection(array('getName')); - $connection->expects($this->once())->method('getName')->will($this->returnValue('name')); - $connection->setEventDispatcher($events = M::mock('Illuminate\Contracts\Events\Dispatcher')); - $events->shouldReceive('dispatch')->once()->with('connection.name.beganTransaction', $connection); - $connection->beginTransaction(); - } - - public function testCommitedFiresEventsIfSet() - { - $connection = $this->getMockConnection(array('getName')); - $connection->expects($this->once())->method('getName')->will($this->returnValue('name')); - $connection->setEventDispatcher($events = M::mock('Illuminate\Contracts\Events\Dispatcher')); - $events->shouldReceive('dispatch')->once()->with('connection.name.committed', $connection); - $connection->commit(); - } - - public function testRollBackedFiresEventsIfSet() - { - $connection = $this->getMockConnection(array('getName')); - $connection->expects($this->once())->method('getName')->will($this->returnValue('name')); - $connection->setEventDispatcher($events = M::mock('Illuminate\Contracts\Events\Dispatcher')); - $events->shouldReceive('dispatch')->once()->with('connection.name.rollingBack', $connection); - $connection->rollback(); - } - - public function testTransactionMethodRunsSuccessfully() - { - $connection = $this->getMockConnection(); - $connection->setClient($this->getClient()); - - $result = $connection->transaction(function ($db) { return $db; }); - $this->assertEquals($connection, $result); - } - - public function testTransactionMethodRollsbackAndThrows() - { - $connection = $this->getMockConnection(); - $connection->setClient($this->getClient()); - - try { - $connection->transaction(function () { throw new \Exception('foo'); }); - } catch (\Exception $e) { - $this->assertEquals('foo', $e->getMessage()); - } - } - - public function testFromCreatesNewQueryBuilder() - { - $conn = $this->getMockConnection(); - $conn->setQueryGrammar(M::mock('Vinelab\NeoEloquent\Query\Grammars\CypherGrammar')->makePartial()); - $builder = $conn->node('User'); - $this->assertInstanceOf('Vinelab\NeoEloquent\Query\Builder', $builder); - $this->assertEquals('User', $builder->from); - } - - /* - * Utility methods below this line - */ - - public function createUser() - { - $c = $this->getConnectionWithConfig('default'); - - // First we create the record that we need to update - $create = 'CREATE (u:User {name: $name, email: $email, username: $username})'; - // The bindings structure is a little weird, I know - // but this is how they are collected internally - // so bare with it =) - $createCypher = $c->getCypherQuery($create, array( - 'name' => $this->user['name'], - 'email' => $this->user['email'], - 'username' => $this->user['username'], - )); - - return $this->client->run($createCypher['statement'], $createCypher['parameters']); - } - - protected function getMockConnection($methods = array()) - { - $defaults = array('getDefaultQueryGrammar', 'getDefaultPostProcessor', 'getDefaultSchemaGrammar'); - - return $this->getMockBuilder('Vinelab\NeoEloquent\Connection') - ->setMethods(array_merge($defaults, $methods)) - ->setConstructorArgs( array($this->dbConfig['connections']['neo4j'])) - ->getMock(); - - } -} - -class DatabaseConnectionTestMockNeo -{ -} diff --git a/tests/Vinelab/NeoEloquent/Eloquent/EloquentBuilderTest.php b/tests/Vinelab/NeoEloquent/Eloquent/EloquentBuilderTest.php deleted file mode 100644 index 333c8e6c..00000000 --- a/tests/Vinelab/NeoEloquent/Eloquent/EloquentBuilderTest.php +++ /dev/null @@ -1,675 +0,0 @@ -query = M::mock('Vinelab\NeoEloquent\Query\Builder'); - $this->query->shouldReceive('modelAsNode')->andReturn('n'); - $this->model = M::mock('Vinelab\NeoEloquent\Eloquent\Model'); - - $this->builder = new Builder($this->query); - } - - public function tearDown(): void - { - M::close(); - - parent::tearDown(); - } - - public function testFindMethod() - { - $builder = M::mock('Vinelab\NeoEloquent\Eloquent\Builder[first]', array($this->getMockQueryBuilder())); - $builder->setModel($this->getMockModel()); - $builder->getQuery()->shouldReceive('where')->once()->with('foo', '=', 'bar'); - $builder->shouldReceive('first')->with(array('column'))->andReturn('baz'); - - $result = $builder->find('bar', array('column')); - $this->assertEquals('baz', $result); - } - - public function testFindOrFailMethodThrowsModelNotFoundException() - { - $builder = m::mock('Vinelab\NeoEloquent\Eloquent\Builder[first]', array($this->getMockQueryBuilder())); - $builder->setModel($this->getMockModel()); - $builder->getQuery()->shouldReceive('where')->once()->with('foo', '=', 'bar'); - $builder->shouldReceive('first')->with(array('column'))->andReturn(null); - - $this->expectException(ModelNotFoundException::class); - $result = $builder->findOrFail('bar', array('column')); - } - - public function testFindOrFailMethodWithManyThrowsModelNotFoundException() - { - $builder = m::mock('Vinelab\NeoEloquent\Eloquent\Builder[get]', array($this->getMockQueryBuilder())); - $builder->setModel($this->getMockModel()); - $builder->getQuery()->shouldReceive('whereIn')->once()->with('foo', [1, 2]); - $builder->shouldReceive('get')->with(array('column'))->andReturn(new Collection([1])); - $this->expectException(ModelNotFoundException::class); - $result = $builder->findOrFail([1, 2], array('column')); - } - - - public function testFirstOrFailMethodThrowsModelNotFoundException() - { - $builder = m::mock('Vinelab\NeoEloquent\Eloquent\Builder[first]', array($this->getMockQueryBuilder())); - $builder->setModel($this->getMockModel()); - $builder->shouldReceive('first')->with(array('column'))->andReturn(null); - $this->expectException(ModelNotFoundException::class); - $result = $builder->firstOrFail(array('column')); - } - - public function testFindWithMany() - { - $builder = m::mock('Vinelab\NeoEloquent\Eloquent\Builder[get]', array($this->getMockQueryBuilder())); - $builder->getQuery()->shouldReceive('whereIn')->once()->with('foo', array(1, 2)); - $builder->setModel($this->getMockModel()); - $builder->shouldReceive('get')->with(array('column'))->andReturn('baz'); - - $result = $builder->find(array(1, 2), array('column')); - $this->assertEquals('baz', $result); - } - - public function testFirstMethod() - { - $builder = m::mock('Vinelab\NeoEloquent\Eloquent\Builder[get,take]', array($this->getMockQueryBuilder())); - $builder->shouldReceive('take')->with(1)->andReturn($builder); - $builder->shouldReceive('get')->with(array('*'))->andReturn(new Collection(array('bar'))); - - $result = $builder->first(); - $this->assertEquals('bar', $result); - } - - public function testGetMethodLoadsModelsAndHydratesEagerRelations() - { - $builder = m::mock('Vinelab\NeoEloquent\Eloquent\Builder[getModels,eagerLoadRelations]', array($this->getMockQueryBuilder())); - $builder->shouldReceive('getModels')->with(array('foo'))->andReturn(array('bar')); - $builder->shouldReceive('eagerLoadRelations')->with(array('bar'))->andReturn(array('bar', 'baz')); - $builder->setModel($this->getMockModel()); - $builder->getModel()->shouldReceive('newCollection')->with(array('bar', 'baz'))->andReturn(new Collection(array('bar', 'baz'))); - - $results = $builder->get(array('foo')); - $this->assertEquals(array('bar', 'baz'), $results->all()); - } - - public function testGetMethodDoesntHydrateEagerRelationsWhenNoResultsAreReturned() - { - $builder = m::mock('Vinelab\NeoEloquent\Eloquent\Builder[getModels,eagerLoadRelations]', array($this->getMockQueryBuilder())); - $builder->shouldReceive('getModels')->with(array('foo'))->andReturn(array()); - $builder->shouldReceive('eagerLoadRelations')->never(); - $builder->setModel($this->getMockModel()); - $builder->getModel()->shouldReceive('newCollection')->with(array())->andReturn(new Collection(array())); - - $results = $builder->get(array('foo')); - $this->assertEquals(array(), $results->all()); - } - - public function testPluckMethodWithModelFound() - { - $builder = m::mock('Vinelab\NeoEloquent\Eloquent\Builder[first]', array($this->getMockQueryBuilder())); - $mockModel = new StdClass(); - $mockModel->name = 'foo'; - $builder->shouldReceive('first')->with(array('name'))->andReturn($mockModel); - - $this->assertEquals('foo', $builder->pluck('name')); - } - - public function testPluckMethodWithModelNotFound() - { - $builder = m::mock('Vinelab\NeoEloquent\Eloquent\Builder[first]', array($this->getMockQueryBuilder())); - $builder->shouldReceive('first')->with(array('name'))->andReturn(null); - - $this->assertNull($builder->pluck('name')); - } - - public function testChunkExecuteCallbackOverPaginatedRequest() - { - $builder = m::mock('Vinelab\NeoEloquent\Eloquent\Builder[forPage,get]', array($this->getMockQueryBuilder())); - $builder->shouldReceive('forPage')->once()->with(1, 2)->andReturn($builder); - $builder->shouldReceive('forPage')->once()->with(2, 2)->andReturn($builder); - $builder->shouldReceive('forPage')->once()->with(3, 2)->andReturn($builder); - $builder->shouldReceive('get')->times(3)->andReturn(array('foo1', 'foo2'), array('foo3'), array()); - - $callbackExecutionAssertor = m::mock('StdClass'); - $callbackExecutionAssertor->shouldReceive('doSomething')->with('foo1')->once(); - $callbackExecutionAssertor->shouldReceive('doSomething')->with('foo2')->once(); - $callbackExecutionAssertor->shouldReceive('doSomething')->with('foo3')->once(); - - $builder->chunk(2, function ($results) use ($callbackExecutionAssertor) { - foreach ($results as $result) { - $callbackExecutionAssertor->doSomething($result); - } - }); - - self::assertTrue(true); - } - - public function testListsReturnsTheMutatedAttributesOfAModel() - { - $builder = $this->getBuilder(); - $builder->getQuery()->shouldReceive('lists')->with('name', '')->andReturn(array('bar', 'baz')); - $builder->setModel($this->getMockModel()); - $builder->getModel()->shouldReceive('hasGetMutator')->with('name')->andReturn(true); - $builder->getModel()->shouldReceive('newFromBuilder')->with(array('name' => 'bar'))->andReturn(new EloquentBuilderTestListsStub(array('name' => 'bar'))); - $builder->getModel()->shouldReceive('newFromBuilder')->with(array('name' => 'baz'))->andReturn(new EloquentBuilderTestListsStub(array('name' => 'baz'))); - - $this->assertEquals(new Collection(['foo_bar', 'foo_baz']), $builder->lists('name')); - } - - public function testListsWithoutModelGetterJustReturnTheAttributesFoundInDatabase() - { - $builder = $this->getBuilder(); - $builder->getQuery()->shouldReceive('lists')->with('name', '')->andReturn(array('bar', 'baz')); - $builder->setModel($this->getMockModel()); - $builder->getModel()->shouldReceive('hasGetMutator')->with('name')->andReturn(false); - - $this->assertEquals(new Collection(['bar', 'baz']), $builder->lists('name')); - } - - public function testGetModelsProperlyHydratesModels() - { - $query = $this->getMockQueryBuilder(); - $query->columns = array('n.name', 'n.age'); - - $builder = M::mock('Vinelab\NeoEloquent\Eloquent\Builder[get]', array($query)); - - $records[] = array('id' => 1902, 'name' => 'taylor', 'age' => 26); - $records[] = array('id' => 6252, 'name' => 'dayle', 'age' => 28); - - $resultSet = $this->createNodeResultSet($records, array('n.name', 'n.age')); - - $builder->getQuery()->shouldReceive('get')->once()->with(array('foo'))->andReturn($resultSet); - $grammar = M::mock('Vinelab\NeoEloquent\Query\Grammars\CypherGrammar')->makePartial(); - $builder->getQuery()->shouldReceive('getGrammar')->andReturn($grammar); - - $model = M::mock('Vinelab\NeoEloquent\Eloquent\Model[nodeLabel,getConnectionName,newInstance]'); - $model->shouldReceive('nodeLabel')->once()->andReturn('foo_table'); - - $builder->setModel($model); - - $model->shouldReceive('getConnectionName')->once()->andReturn('foo_connection'); - $model->shouldReceive('newInstance')->andReturnUsing(function () { return new EloquentBuilderTestModelStub(); }); - $models = $builder->getModels(array('foo')); - - $this->assertEquals('taylor', $models[0]->name); - $this->assertEquals($models[0]->getAttributes(), $models[0]->getOriginal()); - $this->assertEquals('dayle', $models[1]->name); - $this->assertEquals($models[1]->getAttributes(), $models[1]->getOriginal()); - $this->assertEquals('foo_connection', $models[0]->getConnectionName()); - $this->assertEquals('foo_connection', $models[1]->getConnectionName()); - } - - public function testEagerLoadRelationsLoadTopLevelRelationships() - { - $builder = m::mock('Vinelab\NeoEloquent\Eloquent\Builder[loadRelation]', array($this->getMockQueryBuilder())); - $nop1 = function () {}; - $nop2 = function () {}; - $builder->setEagerLoads(array('foo' => $nop1, 'foo.bar' => $nop2)); - $builder->shouldAllowMockingProtectedMethods()->shouldReceive('loadRelation')->with(array('models'), 'foo', $nop1)->andReturn(array('foo')); - - $results = $builder->eagerLoadRelations(array('models')); - $this->assertEquals(array('foo'), $results); - } - - public function testGetRelationProperlySetsNestedRelationships() - { - $builder = $this->getBuilder(); - $builder->setModel($this->getMockModel()); - $builder->getModel()->shouldReceive('orders')->once()->andReturn($relation = m::mock('stdClass')); - $relationQuery = m::mock('stdClass'); - $relation->shouldReceive('getQuery')->andReturn($relationQuery); - $relationQuery->shouldReceive('with')->once()->with(array('lines' => null, 'lines.details' => null)); - $builder->setEagerLoads(array('orders' => null, 'orders.lines' => null, 'orders.lines.details' => null)); - - $relation = $builder->getRelation('orders'); - - self::assertInstanceOf(stdClass::class, $relation); - } - - public function testGetRelationProperlySetsNestedRelationshipsWithSimilarNames() - { - $builder = $this->getBuilder(); - $builder->setModel($this->getMockModel()); - $builder->getModel()->shouldReceive('orders')->once()->andReturn($relation = m::mock('stdClass')); - $builder->getModel()->shouldReceive('ordersGroups')->once()->andReturn($groupsRelation = m::mock('stdClass')); - - $relationQuery = m::mock('stdClass'); - $relation->shouldReceive('getQuery')->andReturn($relationQuery); - - $groupRelationQuery = m::mock('stdClass'); - $groupsRelation->shouldReceive('getQuery')->andReturn($groupRelationQuery); - $groupRelationQuery->shouldReceive('with')->once()->with(array('lines' => null, 'lines.details' => null)); - - $builder->setEagerLoads(array('orders' => null, 'ordersGroups' => null, 'ordersGroups.lines' => null, 'ordersGroups.lines.details' => null)); - - $relation = $builder->getRelation('orders'); - $relation = $builder->getRelation('ordersGroups'); - - self::assertInstanceOf(stdClass::class, $relation); - } - - public function testEagerLoadParsingSetsProperRelationships() - { - $builder = $this->getBuilder(); - $builder->with(array('orders', 'orders.lines')); - $eagers = $builder->getEagerLoads(); - - $this->assertEquals(array('orders', 'orders.lines'), array_keys($eagers)); - $this->assertInstanceOf('Closure', $eagers['orders']); - $this->assertInstanceOf('Closure', $eagers['orders.lines']); - - $builder = $this->getBuilder(); - $builder->with('orders', 'orders.lines'); - $eagers = $builder->getEagerLoads(); - - $this->assertEquals(array('orders', 'orders.lines'), array_keys($eagers)); - $this->assertInstanceOf('Closure', $eagers['orders']); - $this->assertInstanceOf('Closure', $eagers['orders.lines']); - - $builder = $this->getBuilder(); - $builder->with(array('orders.lines')); - $eagers = $builder->getEagerLoads(); - - $this->assertEquals(array('orders', 'orders.lines'), array_keys($eagers)); - $this->assertInstanceOf('Closure', $eagers['orders']); - $this->assertInstanceOf('Closure', $eagers['orders.lines']); - - $builder = $this->getBuilder(); - $builder->with(array('orders' => function () { return 'foo'; })); - $eagers = $builder->getEagerLoads(); - - $this->assertEquals('foo', $eagers['orders']()); - - $builder = $this->getBuilder(); - $builder->with(array('orders.lines' => function () { return 'foo'; })); - $eagers = $builder->getEagerLoads(); - - $this->assertInstanceOf('Closure', $eagers['orders']); - $this->assertNull($eagers['orders']()); - $this->assertEquals('foo', $eagers['orders.lines']()); - } - - public function testQueryPassThru() - { - $builder = $this->getBuilder(); - $model = \Vinelab\NeoEloquent\Eloquent\Model::class; - $model = M::mock($model); - $model->shouldReceive('nodeLabel')->once()->andReturn('Model'); - $builder->setModel($model); - $builder->getQuery()->shouldReceive('foobar')->once()->andReturn('foo'); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Builder', $builder->foobar()); - - $builder = $this->getBuilder(); - $model = \Vinelab\NeoEloquent\Eloquent\Model::class; - $model = M::mock($model); - $model->shouldReceive('nodeLabel')->once()->andReturn('Model'); - $builder->setModel($model); - $builder->getQuery()->shouldReceive('insert')->once()->with(array('bar'))->andReturn('foo'); - - $this->assertEquals('foo', $builder->insert(array('bar'))); - } - - public function testQueryScopes() - { - $builder = $this->getBuilder(); - $builder->getQuery()->shouldReceive('from'); - $builder->getQuery()->shouldReceive('where')->once()->with('foo', 'bar'); - $builder->setModel($model = new EloquentBuilderTestScopeStub()); - $result = $builder->approved(); - - $this->assertEquals($builder, $result); - } - - public function testSimpleWhere() - { - $builder = $this->getBuilder(); - $builder->getQuery()->shouldReceive('where')->once()->with('foo', '=', 'bar'); - $result = $builder->where('foo', '=', 'bar'); - $this->assertEquals($result, $builder); - } - - public function testNestedWhere() - { - $this->markTestIncomplete('Getting error: Static method Mockery_1_Vinelab_NeoEloquent_Eloquent_Model::resolveConnection() does not exist on this mock object'); - - $nestedQuery = m::mock('Vinelab\NeoEloquent\Eloquent\Builder'); - $nestedRawQuery = $this->getMockQueryBuilder(); - $nestedQuery->shouldReceive('getQuery')->once()->andReturn($nestedRawQuery); - $model = $this->getMockModel()->makePartial(); - $model->shouldReceive('newQueryWithoutScopes')->once()->andReturn($nestedQuery); - $builder = $this->getBuilder(); - $builder->getQuery()->shouldReceive('from'); - $builder->setModel($model); - $builder->getQuery()->shouldReceive('addNestedWhereQuery')->once()->with($nestedRawQuery, 'and'); - $nestedQuery->shouldReceive('foo')->once(); - - $result = $builder->where(function ($query) { $query->foo(); }); - $this->assertEquals($builder, $result); - } - - public function testDeleteOverride() - { - $this->markTestIncomplete('Getting the error BadMethodCallException: Method Mockery_2_Vinelab_NeoEloquent_Query_Builder::onDelete() does not exist on this mock object'); - $builder = $this->getBuilder(); - $builder->onDelete(function ($builder) { - return array('foo' => $builder); - }); - $this->assertEquals(array('foo' => $builder), $builder->delete()); - } - - public function testFindingById() - { - $this->query->shouldReceive('getGrammar')->andReturn(new CypherGrammar()); - - $resultSet = new CypherList([ new CypherMap(['node' => new \Laudis\Neo4j\Types\Node(1, new CypherList(), new CypherMap())])]); - - $this->query->shouldReceive('where')->once()->with('id(n)', '=', 1); - $this->query->shouldReceive('from')->once()->with('Model')->andReturn(array('Model')); - $this->query->shouldReceive('take')->once()->with(1)->andReturn($this->query); - $this->query->shouldReceive('get')->once()->with(array('*'))->andReturn($resultSet); - - $this->model->shouldReceive('getKeyName')->times()->andReturn('id'); - $this->model->shouldReceive('nodeLabel')->once()->andReturn('Model'); - $this->model->shouldReceive('getConnectionName')->once()->andReturn('default'); - - $result = M::mock('Neoxygen\NeoClient\Formatter\Result'); - $collection = new \Illuminate\Support\Collection(array($result)); - $this->model->shouldReceive('newCollection')->once()->andReturn($collection); - $this->model->shouldReceive('getAttributes')->once()->andReturn([]); - $this->model->shouldReceive('setConnection')->once(); - $this->model->shouldReceive('newFromBuilder')->once()->andReturn($this->model); - - $this->builder->setModel($this->model); - - $result = $this->builder->find(1); - - $this->assertInstanceOf('Neoxygen\NeoClient\Formatter\Result', $result); - } - - public function testFindingByIdWithProperties() - { - // the intended Node id - $id = 6; - - // the expected result set - $result = array( - - 'id' => $id, - 'name' => 'Some Name', - 'email' => 'some@mail.net', - ); - - // the properties that we need returned of our model - $properties = array('id(n)', 'n.name', 'n.email', 'n.somthing'); - - $resultSet = $this->createNodeResultSet($result, $properties); - - // usual query expectations - $this->query->shouldReceive('where')->once()->with('id(n)', '=', $id) - ->shouldReceive('take')->once()->with(1)->andReturn($this->query) - ->shouldReceive('get')->once()->with($properties)->andReturn($resultSet) - ->shouldReceive('from')->once()->with('Model') - ->andReturn(array('Model')); - - // our User object that we expect to have returned - $user = M::mock('User'); - $user->shouldReceive('setConnection')->once()->with('default'); - - // model method calls expectations - $attributes = array_merge($result, array('id' => $id)); - - // the Collection that represents the returned result by Eloquent holding the User as an item - $collection = new \Illuminate\Support\Collection(array($user)); - - $this->model->shouldReceive('newCollection')->once()->andReturn($collection) - ->shouldReceive('getKeyName')->times(3)->andReturn('id') - ->shouldReceive('nodeLabel')->once()->andReturn('Model') - ->shouldReceive('getConnectionName')->once()->andReturn('default') - ->shouldReceive('newFromBuilder')->once()->with($attributes)->andReturn($user); - - // assign the builder's $model to our mock - $this->builder->setModel($this->model); - $grammar = M::mock('Vinelab\NeoEloquent\Query\Grammars\CypherGrammar')->makePartial(); - $this->query->shouldReceive('getGrammar')->andReturn($grammar); - // put things to the test - $this->model->shouldReceive('getAttributes')->once()->andReturn([]); - $found = $this->builder->find($id, $properties); - - $this->assertInstanceOf('User', $found); - } - - public function testGettingModels() - { - // the expected result set - $results = array( - - array( - - 'id' => 10, - 'name' => 'Some Name', - 'email' => 'some@mail.net', - ), - - array( - - 'id' => 11, - 'name' => 'Another Person', - 'email' => 'person@diff.io', - ), - - ); - - $resultSet = $this->createNodeResultSet($results); - - $grammar = M::mock('Vinelab\NeoEloquent\Query\Grammars\CypherGrammar')->makePartial(); - $this->query->shouldReceive('get')->once()->with(array('*'))->andReturn($resultSet) - ->shouldReceive('from')->once()->andReturn('User') - ->shouldReceive('getGrammar')->andReturn($grammar); - - // our User object that we expect to have returned - $user = M::mock('User'); - $user->shouldReceive('setConnection')->twice()->with('default'); - - $this->model->shouldReceive('nodeLabel')->once()->andReturn('User') - ->shouldReceive('getKeyName')->twice()->andReturn('id') - ->shouldReceive('getConnectionName')->once()->andReturn('default') - ->shouldReceive('newFromBuilder')->once() - ->with($results[0])->andReturn($user) - ->shouldReceive('newFromBuilder')->once() - ->with($results[1])->andReturn($user) - ->shouldReceive('getAttributes')->andReturn([]); - - $this->builder->setModel($this->model); - - $models = $this->builder->getModels(); - - $this->assertIsArray($models); - $this->assertInstanceOf('User', $models[0]); - $this->assertInstanceOf('User', $models[1]); - } - - public function testGettingModelsWithProperties() - { - // the expected result set - $results = array( - 'id' => 138, - 'name' => 'Nicolas Jaar', - 'email' => 'noise@space.see', - ); - - $properties = array('id', 'name'); - - $resultSet = $this->createNodeResultSet($results); - - $grammar = M::mock('Vinelab\NeoEloquent\Query\Grammars\CypherGrammar')->makePartial(); - $this->query->shouldReceive('get')->once()->with($properties)->andReturn($resultSet) - ->shouldReceive('from')->once()->andReturn('User') - ->shouldReceive('getGrammar')->andReturn($grammar); - - // our User object that we expect to have returned - $user = M::mock('User'); - $user->shouldReceive('setConnection')->once()->with('default'); - - $this->model->shouldReceive('nodeLabel')->once()->andReturn('User') - ->shouldReceive('getKeyName')->once()->andReturn('id') - ->shouldReceive('getConnectionName')->once()->andReturn('default') - ->shouldReceive('newFromBuilder')->once() - ->with($results)->andReturn($user) - ->shouldReceive('getAttributes')->once()->andReturn([]); - - $this->builder->setModel($this->model); - - $models = $this->builder->getModels($properties); - - $this->assertIsArray($models); - $this->assertInstanceOf('User', $models[0]); - } - - public function testCheckingIsRelationship() - { - $this->assertTrue($this->builder->isRelationship(['user', 'account'])); - $this->assertFalse($this->builder->isRelationship(['user.name', 'account.id'])); - $this->assertFalse($this->builder->isRelationship(['user', 'user.name', 'account.id'])); - } - - /** - * Utility methods down below this area. - */ - - /** - * Create a new ResultSet out of an array of properties and values. - * - * @param array $data The values you want returned could be of the form - * [ [name => something, username => here] ] - * or specify the attributes straight in the array - * @param array $properties The expected properties (columns) - * - * @return CypherList - */ - public function createNodeResultSet($data = array(), $properties = array()) - { - $result = []; - - if (is_array(reset($data))) { - foreach ($data as $index => $attributes) { - $result[] = new CypherMap(['node' => $this->createNode($attributes)]); - } - } else { - $node = $this->createNode($data); - $result[] = new CypherMap(['node' => $node]); - } - - // the ResultSet $result part - - return new CypherList($result); - } - - /** - * Get a row with a Node inside of it having $data as properties. - * - * @param array $data - * - * @return \Laudis\Neo4j\Types\Node - */ - public function createNode(array $data) - { - return new \Laudis\Neo4j\Types\Node($data['id'], new CypherList(), new CypherMap($data)); - } - - public function createRowWithPropertiesAtIndex($index, array $properties) - { - $row = M::mock('Everyman\Neo4j\Query\Row'); - // $row->shouldReceive('offsetGet')->with($index)->andReturn($properties); - - foreach ($properties as $key => $value) { - // prepare the row's offsetGet to rerturn the desired value when asked - // by prepending the key with an n. representing the node in the Cypher query. - $row->shouldReceive('offsetGet') - ->with("n.{$key}") - ->andReturn($properties[$key]); - - $row->shouldReceive('offsetGet') - ->with("{$key}") - ->andReturn($properties[$key]); - } - - return $row; - } - - protected function getMockModel() - { - $model = m::mock('Vinelab\NeoEloquent\Eloquent\Model'); - $model->shouldReceive('getKeyName')->andReturn('foo'); - $model->shouldReceive('nodeLabel')->andReturn('foo_table'); - $model->shouldReceive('getQualifiedKeyName')->andReturn('foo'); - - return $model; - } - - protected function getMockQueryBuilder() - { - $query = m::mock('Vinelab\NeoEloquent\Query\Builder'); - $query->shouldReceive('from')->with('foo_table'); - $query->shouldReceive('modelAsNode')->andReturn('n'); - - return $query; - } - - protected function getBuilder() - { - $query = M::mock('Vinelab\NeoEloquent\Query\Builder'); - $query->shouldReceive('from')->andReturn('foo_table'); - $query->shouldReceive('modelAsNode')->andReturn('n'); - return new Builder($query); - } -} - -// Don't ask what this is, brought in from -// laravel/framework/tests/Databases/DatabaseEloquentBuilderTest.php -// and it makes the tests pass, so leave it :P -class EloquentBuilderTestModelStub extends \Vinelab\NeoEloquent\Eloquent\Model -{ -} - -class EloquentBuilderTestScopeStub extends \Vinelab\NeoEloquent\Eloquent\Model -{ - public function scopeApproved($query) - { - $query->where('foo', 'bar'); - } -} - -class EloquentBuilderTestListsStub -{ - protected $attributes; - - public function __construct($attributes) - { - $this->attributes = $attributes; - } - public function __get($key) - { - return 'foo_'.$this->attributes[$key]; - } -} diff --git a/tests/Vinelab/NeoEloquent/Eloquent/ModelTest.php b/tests/Vinelab/NeoEloquent/Eloquent/ModelTest.php deleted file mode 100644 index 006555e0..00000000 --- a/tests/Vinelab/NeoEloquent/Eloquent/ModelTest.php +++ /dev/null @@ -1,127 +0,0 @@ -getDefaultNodeLabel(); - - // By default the label should be the concatenation of the class's namespace - $this->assertEquals('VinelabNeoEloquentTestsEloquentModel', reset($label)); - } - - public function testOverriddenNodeLabel() - { - $m = new Labeled(); - - $label = $m->getDefaultNodeLabel(); - - $this->assertEquals('Labeled', reset($label)); - } - - public function testLabelBackwardCompatibilityWithTable() - { - $m = new Table(); - - $label = $m->nodeLabel(); - - $this->assertEquals('Table', reset($label)); - } - - public function testSettingLabelAtRuntime() - { - $m = new Model(); - - $m->setLabel('Padrouga'); - - $label = $m->getDefaultNodeLabel(); - - $this->assertEquals('Padrouga', reset($label)); - } - - public function testDifferentTypesOfLabelsAlwaysLandsAnArray() - { - $m = new Model(); - - $m->setLabel(array('User', 'Fan')); - $label = $m->getDefaultNodeLabel(); - $this->assertEquals(array('User', 'Fan'), $label); - - $m->setLabel(':User:Fan'); - $label = $m->getDefaultNodeLabel(); - $this->assertEquals(array('User', 'Fan'), $label); - - $m->setLabel('User:Fan:Maker:Baker'); - $label = $m->getDefaultNodeLabel(); - $this->assertEquals(array('User', 'Fan', 'Maker', 'Baker'), $label); - } - - public function testGettingEloquentBuilder() - { - $m = new Model(); - - $builder = $m->newEloquentBuilder(M::mock('Vinelab\NeoEloquent\Query\Builder')); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Builder', $builder); - } - - public function testAddLabels() - { - //create a new model object - $m = new Labeled(); - $m->setLabel(array('User', 'Fan')); //set some labels - $m->save(); - //get the node id, we need it to verify if the label is actually added in graph - $id = $m->id; - - //add the label - $m->addLabels(array('Superuniqelabel1')); - - $labels = $this->getNodeLabels($id); - - $this->assertTrue(in_array('Superuniqelabel1', $labels)); - } - - public function testDropLabels() - { - //create a new model object - $m = new Labeled(); - $m->setLabel(array('User', 'Fan', 'Superuniqelabel2')); //set some labels - $m->save(); - //get the node id, we need it to verify if the label is actually added in graph - $id = $m->id; - - //drop the label - $m->dropLabels(array('Superuniqelabel2')); - $this->assertFalse(in_array('Superuniqelabel2', $this->getNodeLabels($id))); - } -} diff --git a/tests/Vinelab/NeoEloquent/Eloquent/Relations/BelongsToTest.php b/tests/Vinelab/NeoEloquent/Eloquent/Relations/BelongsToTest.php deleted file mode 100644 index ddf32c29..00000000 --- a/tests/Vinelab/NeoEloquent/Eloquent/Relations/BelongsToTest.php +++ /dev/null @@ -1,157 +0,0 @@ -shouldReceive('connection')->andReturn($this->getConnectionWithConfig('default')); - Stub::setConnectionResolver($resolver); - } - - public function testRelationInitializationAddsConstraints() - { - $relation = $this->getRelation(); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Relations\BelongsTo', $relation); - } - - public function testUpdateMethodRetrievesModelAndUpdates() - { - $relation = $this->getRelation(); - $mock = M::mock('Vinelab\NeoEloquent\Eloquent\Model'); - $mock->shouldReceive('fill')->once()->with(array('attributes'))->andReturn($mock); - $mock->shouldReceive('save')->once()->andReturn(true); - $relation->getQuery()->shouldReceive('first')->once()->andReturn($mock); - - $this->assertTrue($relation->update(array('attributes'))); - } - - public function testEagerConstraintsAreProperlyAdded() - { - $models = [new Stub(['id' => 1]), new Stub(['id' => 2]), new Stub(['id' => 3])]; - $relation = $this->getEagerRelation($models); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Relations\BelongsTo', $relation); - } - - public function testRelationIsProperlyInitialized() - { - $relation = $this->getRelation(); - $model = M::mock('Vinelab\NeoEloquent\Eloquent\Model'); - $model->shouldReceive('setRelation')->once()->with('foo', null); - $models = $relation->initRelation(array($model), 'foo'); - - $this->assertEquals(array($model), $models); - } - - public function testModelsAreProperlyMatchedToParents() - { - $this->markTestIncomplete('We should be testing mutations'); - - $relation = $this->getRelation(); - $result1 = M::mock('stdClass'); - $result1->shouldReceive('getAttribute')->with('id')->andReturn(1); - $result2 = M::mock('stdClass'); - $result2->shouldReceive('getAttribute')->with('id')->andReturn(2); - $model1 = new Stub(); - $model1->foreign_key = 1; - $model2 = new Stub(); - $model2->foreign_key = 2; - $models = $relation->match(array($model1, $model2), new Collection(array($result1, $result2)), 'foo'); - - $this->assertEquals(1, $models[0]->foo->getAttribute('id')); - $this->assertEquals(2, $models[1]->foo->getAttribute('id')); - } - - public function testMutationsAreProperlySet() - { - $this->markTestIncomplete(); - } - - protected function getEagerRelation($models) - { - $query = M::mock('Vinelab\NeoEloquent\Query\Builder'); - $query->shouldReceive('modelAsNode')->with(array('Stub'))->andReturn('parent'); - - $builder = M::mock('Vinelab\NeoEloquent\Eloquent\Builder'); - $builder->shouldReceive('getQuery')->times(4)->andReturn($query); - $builder->shouldReceive('select')->once()->with('relation'); - $builder->shouldReceive('select')->once()->with('relation', 'parent'); - - $related = M::mock('Vinelab\NeoEloquent\Eloquent\Model')->makePartial(); - $related->shouldReceive('getKeyName')->andReturn('id'); - $related->shouldReceive('nodeLabel')->andReturn('relation'); - - $id = 19; - $parent = new Stub(['id' => $id]); - - $builder->shouldReceive('getModel')->once()->andReturn($related); - $builder->shouldReceive('addMutation')->once()->with('relation', $related); - $builder->shouldReceive('addMutation')->once()->with('parent', $parent); - - $builder->shouldReceive('where')->once()->with('id', '=', $id); - - $builder->shouldReceive('matchIn')->twice() - ->with($parent, $related, 'relation', 'RELATIONSHIP', 'id', $id); - - $relation = new belongsTo($builder, $parent, 'RELATIONSHIP', 'id', 'relation'); - - $builder->shouldReceive('whereIn')->once() - ->with('id', array_map(function ($model) { return $model->id; }, $models)); - - $relation->addEagerConstraints($models); - - return $relation; - } - - protected function getRelation($parent = null) - { - $query = M::mock('Vinelab\NeoEloquent\Query\Builder'); - $query->shouldReceive('modelAsNode')->with(array('Stub'))->andReturn('parent'); - - $builder = M::mock('Vinelab\NeoEloquent\Eloquent\Builder'); - $builder->shouldReceive('getQuery')->twice()->andReturn($query); - $builder->shouldReceive('select')->once()->with('relation'); - - $related = M::mock('Vinelab\NeoEloquent\Eloquent\Model')->makePartial(); - $related->shouldReceive('getKeyName')->andReturn('id'); - $related->shouldReceive('nodeLabel')->andReturn('relation'); - - $builder->shouldReceive('getModel')->once()->andReturn($related); - - $id = 19; - $parent = new Stub(['id' => $id]); - - $builder->shouldReceive('matchIn')->once() - ->with($parent, $related, 'relation', 'RELATIONSHIP', 'id', $id); - - $builder->shouldReceive('where')->once() - ->with('id', '=', $id); - - return new belongsTo($builder, $parent, 'RELATIONSHIP', 'id', 'relation'); - } -} - -class Stub extends Model -{ - protected $label = ':Stub'; - - protected $fillable = ['id']; -} diff --git a/tests/Vinelab/NeoEloquent/Query/BuilderTest.php b/tests/Vinelab/NeoEloquent/Query/BuilderTest.php deleted file mode 100644 index bec91b97..00000000 --- a/tests/Vinelab/NeoEloquent/Query/BuilderTest.php +++ /dev/null @@ -1,334 +0,0 @@ -grammar = M::mock('Vinelab\NeoEloquent\Query\Grammars\CypherGrammar')->makePartial(); - $this->connection = M::mock('Vinelab\NeoEloquent\Connection')->makePartial(); - - $this->neoClient = M::mock(ClientInterface::class); - $this->connection->shouldReceive('getClient')->andReturn($this->neoClient); - - $this->builder = new Builder($this->connection, $this->grammar); - } - - public function tearDown(): void - { - M::close(); - - parent::tearDown(); - } - - public function testSettingNodeLabels() - { - $this->builder->from(array('labels')); - $this->assertEquals(array('labels'), $this->builder->from); - - $this->builder->from('User:Fan'); - $this->assertEquals('User:Fan', $this->builder->from); - } - - public function testInsertingAndGettingId() - { - $label = array('Hero'); - $this->builder->from($label); - - $values = array( - 'length' => 123, - 'height' => 343, - 'power' => 'Strong Fart Noises', - ); - - $query = [ - 'statement' => 'CREATE (hero:`Hero`) SET hero.length = $length_create, hero.height = $height_create, hero.power = $power_create RETURN hero', - 'parameters' => [ - 'length_create' => $values['length'], - 'height_create' => $values['height'], - 'power_create' => $values['power'], - ], - ]; - - $id = 69; - $node = new Node($id, new CypherList(['Hero']), new CypherMap($values)); - $result = new CypherList([new CypherMap(['hero' => $node])]); - - $this->neoClient->shouldReceive('run') - ->once() - ->with($query['statement'], $query['parameters']) - ->andReturn(new CypherList($result)); - - $this->assertEquals($id, $this->builder->insertGetId($values)); - } - - public function testTransformingQueryToCypher() - { - $this->grammar->shouldReceive('compileSelect')->once()->with($this->builder)->andReturn(true); - $this->assertTrue($this->builder->toCypher()); - } - - public function testMakingLabel() - { - $label = array('MaLabel'); - - $this->neoClient->shouldReceive('makeLabel')->with($label)->andReturn($label); - $this->assertEquals($label, $this->builder->makeLabel($label)); - } - - /** - * @depends testTransformingQueryToCypher - */ - public function testSelectResult() - { - $cypher = 'Some cypher here'; - $this->grammar->shouldReceive('compileSelect')->once()->andReturn($cypher); - $this->connection->shouldReceive('select')->once() - ->with($cypher, array())->andReturn('result'); - - $result = $this->builder->getFresh(); - - $this->assertEquals($result, 'result'); - } - - /** - * @depends testTransformingQueryToCypher - */ - public function testSelectingProperties() - { - $cypher = 'Some cypher here'; - $this->grammar->shouldReceive('compileSelect')->once()->andReturn($cypher); - $this->connection->shouldReceive('select')->once() - ->with($cypher, array())->andReturn('result'); - - $result = $this->builder->getFresh(array('poop', 'head')); - - $this->assertEquals($result, 'result'); - $this->assertEquals($this->builder->columns, array('poop', 'head'), 'make sure the columns were set'); - } - - - public function testFailingWhereWithNullValue() - { - $this->expectException(InvalidArgumentException::class); - $this->expectErrorMessage('Value must be provided.'); - $this->builder->where('id', '>', null); - } - - public function testBasicWhereBindings() - { - $this->builder->where('id', 19); - - $this->assertEquals(array( - array( - 'type' => 'Basic', - 'column' => 'id(n)', - 'operator' => '=', - 'value' => 19, - 'boolean' => 'and', - 'binding' => 'id(n)', - ), - ), $this->builder->wheres, 'make sure the statement was atted to $wheres'); - // When the '$from' attribute is not set on the query builder, the grammar - // will use 'n' as the default node identifier. - $this->assertEquals(array('idn' => 19), $this->builder->getBindings()); - } - - public function testBasicWhereBindingsWithFromField() - { - $this->builder->from = array('user'); - $this->builder->where('id', 19); - - $this->assertEquals(array( - array( - 'type' => 'Basic', - 'column' => 'id(user)', - 'operator' => '=', - 'value' => 19, - 'boolean' => 'and', - 'binding' => 'id(user)', - ), - ), $this->builder->wheres, 'make sure the statement was atted to $wheres'); - // When no query builder is passed to the grammar then it will return 'n' - // as node identifier by default. - $this->assertEquals(array('iduser' => 19), $this->builder->getBindings()); - } - - public function testNullWhereBindings() - { - $this->builder->where('farted', null); - - $this->assertEquals(array( - array( - 'type' => 'Null', - 'boolean' => 'and', - 'column' => 'farted', - 'binding' => 'farted', - ), - ), $this->builder->wheres); - - $this->assertEmpty($this->builder->getBindings(), 'no bindings should be added when dealing with null stuff..'); - } - - public function testWhereTransformsNodeIdBinding() - { - // when requesting a Node by its id we need to use - // 'id(n)' but that won't be helpful when returned or dealt with - // so we need to tranform it back to 'id' - $this->builder->where('id(n)', 200); - - $this->assertEquals(array( - array( - 'type' => 'Basic', - 'column' => 'id(n)', - 'boolean' => 'and', - 'operator' => '=', - 'value' => 200, - 'binding' => 'id(n)', - ), - ), $this->builder->wheres); - - $this->assertEquals(array('idn' => 200), $this->builder->getBindings()); - } - - public function testNestedWhere() - { - $this->markTestIncomplete('This test has not been implemented yet.'); - } - - public function testSubWhere() - { - $this->markTestIncomplete('This test has not been implemented yet.'); - } - - public function testBasicSelect() - { - $builder = $this->getBuilder(); - $builder->select('*')->from('User'); - $this->assertEquals('MATCH (user:User) RETURN *', $builder->toCypher()); - } - - public function testBasicAlias() - { - $builder = $this->getBuilder(); - $builder->select('foo as bar')->from('User'); - - $this->assertEquals('MATCH (user:User) RETURN user.foo as bar, user', $builder->toCypher()); - } - - public function testAddigSelects() - { - $builder = $this->getBuilder(); - $builder->select('foo')->addSelect('bar')->addSelect(array('baz', 'boom'))->from('User'); - $this->assertEquals('MATCH (user:User) RETURN user.foo, user.bar, user.baz, user.boom, user', $builder->toCypher()); - } - - public function testBasicWheres() - { - $builder = $this->getBuilder(); - $builder->select('*')->from('User')->where('username', '=', 'bakalazma'); - - $bindings = $builder->getBindings(); - $this->assertEquals('MATCH (user:User) WHERE user.username = $userusername RETURN *', $builder->toCypher()); - $this->assertEquals(array('userusername' => 'bakalazma'), $bindings); - } - - public function testBasicSelectDistinct() - { - $builder = $this->getBuilder(); - $builder->distinct()->select('foo', 'bar')->from('User'); - - $this->assertEquals('MATCH (user:User) RETURN DISTINCT user.foo, user.bar, user', $builder->toCypher()); - } - - public function testAddBindingWithArrayMergesBindings() - { - $builder = $this->getBuilder(); - $builder->addBinding(array('foo' => 'bar')); - $builder->addBinding(array('bar' => 'baz')); - - $this->assertEquals(array( - 'foo' => 'bar', - 'bar' => 'baz', - ), $builder->getBindings()); - } - - public function testAddBindingWithArrayMergesBindingsInCorrectOrder() - { - $builder = $this->getBuilder(); - $builder->addBinding(array('bar' => 'baz'), 'having'); - $builder->addBinding(array('foo' => 'bar'), 'where'); - - $this->assertEquals(array( - 'bar' => 'baz', - 'foo' => 'bar', - ), $builder->getBindings()); - } - - public function testMergeBuilders() - { - $builder = $this->getBuilder(); - $builder->addBinding(array('foo' => 'bar')); - - $otherBuilder = $this->getBuilder(); - $otherBuilder->addBinding(array('baz' => 'boom')); - - $builder->mergeBindings($otherBuilder); - - $this->assertEquals(array( - 'foo' => 'bar', - 'baz' => 'boom', - ), $builder->getBindings()); - } - - /* - * Utility functions down this line - */ - - public function setupCacheTestQuery($cache, $driver) - { - $connection = m::mock('Vinelab\NeoEloquent\Connection'); - $connection->shouldReceive('getClient')->once()->andReturn(M::mock('Everyman\Neo4j\Client')); - $connection->shouldReceive('getName')->andReturn('default'); - $connection->shouldReceive('getCacheManager')->once()->andReturn($cache); - $cache->shouldReceive('driver')->once()->andReturn($driver); - $grammar = new CypherGrammar(); - - $builder = $this->getMock('Vinelab\NeoEloquent\Query\Builder', array('getFresh'), array($connection, $grammar)); - $builder->expects($this->once())->method('getFresh')->with($this->equalTo(array('*')))->will($this->returnValue(array('results'))); - - return $builder->select('*')->from('User')->where('email', 'foo@bar.com'); - } - - protected function getBuilder() - { - $connection = M::mock('Vinelab\NeoEloquent\Connection'); - $client = M::mock('Everyman\Neo4j\Client'); - $connection->shouldReceive('getClient')->once()->andReturn($client); - $grammar = new CypherGrammar(); - - return new Builder($connection, $grammar); - } -} diff --git a/tests/Vinelab/NeoEloquent/Query/GrammarTest.php b/tests/Vinelab/NeoEloquent/Query/GrammarTest.php deleted file mode 100644 index 3205268c..00000000 --- a/tests/Vinelab/NeoEloquent/Query/GrammarTest.php +++ /dev/null @@ -1,134 +0,0 @@ -grammar = new CypherGrammar(); - } - - public function tearDown(): void - { - M::close(); - - parent::tearDown(); - } - - public function testGettingQueryParameterFromRegularValue() - { - $p = $this->grammar->parameter('value'); - $this->assertEquals('$value', $p); - } - - public function testGettingIdQueryParameter() - { - $p = $this->grammar->parameter('id'); - $this->assertEquals('$idn', $p); - } - - public function testGettingIdParameterWithQueryBuilder() - { - $query = M::mock('Vinelab\NeoEloquent\Query\Builder'); - $query->from = 'user'; - $this->grammar->setQuery($query); - $this->assertEquals('$iduser', $this->grammar->parameter('id')); - - $query->from = 'post'; - $this->assertEquals('$idpost', $this->grammar->parameter('id')); - - $anotherQuery = M::mock('Vinelab\NeoEloquent\Query\Builder'); - $anotherQuery->from = 'crawler'; - $this->grammar->setQuery($anotherQuery); - $this->assertEquals('$idcrawler', $this->grammar->parameter('id')); - } - - public function testGettingWheresParameter() - { - $this->assertEquals('$confusing', $this->grammar->parameter(['column' => 'confusing'])); - } - - public function testGettingExpressionParameter() - { - $ex = new Expression('id'); - $this->assertEquals('$idn', $this->grammar->parameter($ex)); - } - - public function testPreparingLabel() - { - $this->assertEquals(':`user`', $this->grammar->prepareLabels(['user']), 'case sensitive'); - $this->assertEquals(':`User`:`Artist`:`Official`', $this->grammar->prepareLabels(['User', 'Artist', 'Official']), 'order'); - $this->assertEquals(':`Photo`:`Media`', $this->grammar->prepareLabels([':Photo', 'Media']), 'intelligent with :'); - $this->assertEquals(':`Photo`:`Media`', $this->grammar->prepareLabels(['Photo', ':Media']), 'even more intelligent with :'); - } - - public function testPreparingRelationName() - { - $this->assertEquals('rel_posted_post:POSTED', $this->grammar->prepareRelation('POSTED', 'post')); - } - - public function testNormalizingLabels() - { - $this->assertEquals('labels_and_more', $this->grammar->normalizeLabels(':Labels:And:More')); - $this->assertEquals('labels_and_more', $this->grammar->normalizeLabels('Labels:And:more')); - } - - public function testWrappingValue() - { - $mConnection = M::mock('Vinelab\NeoEloquent\Connection'); - $mConnection->shouldReceive('getClient'); - $query = new Builder($mConnection, $this->grammar); - - $this->assertEquals('n.value', $this->grammar->wrap('value')); - - $query->from = ['user']; - $this->assertEquals('id(user)', $this->grammar->wrap('id'), 'Ids are treated differently'); - $this->assertEquals('user.name', $this->grammar->wrap('name')); - - $this->assertEquals('post.title', $this->grammar->wrap('post.title'), 'do not touch values with dots in them'); - } - - public function testValufying() - { - $this->assertEquals("'val'", $this->grammar->valufy('val')); - $this->assertEquals("'\'va\\\l\''", $this->grammar->valufy("'va\l'")); - $this->assertEquals('\'\\\u123\'', $this->grammar->valufy('\u123')); - $this->assertEquals('\'val/u\'', $this->grammar->valufy('val/u')); - } - - public function testValufyingArrays() - { - $this->assertEquals("['valu1','valu2','valu3']", $this->grammar->valufy(['valu1', 'valu2', 'valu3'])); - - $this->assertEquals('[\'valu\\\1\',\'valu\\\'2\\\'\',\'val/u3\']', $this->grammar->valufy(['valu\1', "valu'2'", 'val/u3'])); - } - - public function testGeneratingNodeIdentifier() - { - $this->assertEquals('n', $this->grammar->modelAsNode()); - $this->assertEquals('user', $this->grammar->modelAsNode('User')); - $this->assertEquals('rock_paper_scissors', $this->grammar->modelAsNode(['Rock', 'Paper', 'Scissors'])); - } - - public function testReplacingIdProperty() - { - $this->assertEquals('idn', $this->grammar->getIdReplacement('id')); - $this->assertEquals('iduser', $this->grammar->getIdReplacement('id(user)')); - - $query = M::mock('Vinelab\NeoEloquent\Query\Builder'); - $query->from = 'cola'; - $this->grammar->setQuery($query); - - $this->assertEquals('idcola', $this->grammar->getIdReplacement('id')); - $this->assertEquals('iddd', $this->grammar->getIdReplacement('id(dd)')); - } -} diff --git a/tests/config/database.php b/tests/config/database.php deleted file mode 100644 index ff1f1af6..00000000 --- a/tests/config/database.php +++ /dev/null @@ -1,25 +0,0 @@ - 'bolt+routing', - - 'connections' => array( - - 'neo4j' => array( - 'driver' => 'neo4j', - 'host' => 'neo4j', - 'port' => 7687, - 'username' => 'neo4j', - 'password' => 'test', - ), - - 'default' => array( - 'driver' => 'neo4j', - 'host' => 'neo4j', - 'port' => 7687, - 'username' => 'neo4j', - 'password' => 'test', - ), - ), -); diff --git a/tests/functional/AddDropLabelsTest.php b/tests/functional/AddDropLabelsTest.php deleted file mode 100644 index 1fdcbfd3..00000000 --- a/tests/functional/AddDropLabelsTest.php +++ /dev/null @@ -1,387 +0,0 @@ -hasOne('Vinelab\NeoEloquent\Tests\Functional\AddDropLabels\Bar', 'OWNS'); - } -} - -class AddDropLabelsTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); - - $resolver = M::mock('Illuminate\Database\ConnectionResolverInterface'); - $resolver->shouldReceive('connection')->andReturn($this->getConnectionWithConfig('default')); - Labelwiz::setConnectionResolver($resolver); - } - - public function tearDown(): void - { - M::close(); - - parent::tearDown(); - } - - public function testAddingDroppingSingleLabelOnNewModel() - { - //create a new model object - $w = new Labelwiz([ - 'fiz' => 'foo', - 'biz' => 'boo', - 'triz' => 'troo', - ]); - $this->assertTrue($w->save()); - - //add the label - $w->addLabels(array('Superuniqelabel1')); - - $nLabels = $this->getNodeLabels($w->id); - $this->assertTrue(in_array('Superuniqelabel1', $nLabels)); - - //now drop the label - $w->dropLabels(array('Superuniqelabel1')); - - // get all the node's labels - $nLabels = $this->getNodeLabels($w->id); - $this->assertFalse(in_array('Superuniqelabel1', $nLabels)); - } - - public function testAddingDroppingLabelsOnNewModel() - { - //create a new model object - $w = new Labelwiz([ - 'fiz' => 'foo1', - 'biz' => 'boo1', - 'triz' => 'troo1', - ]); - $this->assertTrue($w->save()); - - //add the label - $w->addLabels(array('Superuniqelabel3', 'Superuniqelabel4', 'a1')); - - // get all the node's labels - $nLabels = $this->getNodeLabels($w->id); - - $this->assertTrue(in_array('Superuniqelabel3', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel4', $nLabels)); - $this->assertTrue(in_array('a1', $nLabels)); - - //now drop one of the labels - $w->dropLabels(array('a1')); - - // get all the node's labels - $nLabels = $this->getNodeLabels($w->id); - $this->assertFalse(in_array('a1', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel3', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel4', $nLabels)); - - //now drop remaining labels - $w->dropLabels(array('Superuniqelabel3', 'Superuniqelabel4')); - // get all the node's labels - $nLabels = $this->getNodeLabels($w->id); - $this->assertFalse(in_array('a1', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel3', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel4', $nLabels)); - } - - public function testAddDroppLabelsRepeatedlyOnNewModel() - { - //create a new model object - $w = new Labelwiz([ - 'fiz' => 'foo2', - 'biz' => 'boo2', - 'triz' => 'troo2', - ]); - $this->assertTrue($w->save()); - - //add the label - $w->addLabels(array('Superuniqelabel5')); - $w->addLabels(array('Superuniqelabel6')); - $w->addLabels(array('Superuniqelabel7')); - - // get all the node's labels - $nLabels = $this->getNodeLabels($w->id); - - $this->assertTrue(in_array('Superuniqelabel5', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel6', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel7', $nLabels)); - - //now drop repeatedly - $w->dropLabels(array('Superuniqelabel5')); - $w->dropLabels(array('Superuniqelabel6')); - $w->dropLabels(array('Superuniqelabel7')); - - // get all the node's labels - $nLabels = $this->getNodeLabels($w->id); - - $this->assertFalse(in_array('Superuniqelabel5', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel6', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel7', $nLabels)); - } - - public function testAddDropLabelsRepeatedlyOnNewModels() - { - //create a new model object - $w1 = new Labelwiz([ - 'fiz' => 'foo3', - 'biz' => 'boo3', - 'triz' => 'troo4', - ]); - $this->assertTrue($w1->save()); - - //create a new model object - $w2 = new Labelwiz([ - 'fiz' => 'foo4', - 'biz' => 'boo4', - 'triz' => 'troo4', - ]); - $this->assertTrue($w2->save()); - - //create a new model object - $w3 = new Labelwiz([ - 'fiz' => 'foo5', - 'biz' => 'boo5', - 'triz' => 'troo5', - ]); - $this->assertTrue($w3->save()); - - //add the label in sequence - $w1->addLabels(array('Superuniqelabel8')); - $w2->addLabels(array('Superuniqelabel8')); - $w3->addLabels(array('Superuniqelabel8')); - - //add the array of labels - $w1->addLabels(array('Superuniqelabel9', 'Superuniqelabel10')); - $w2->addLabels(array('Superuniqelabel9', 'Superuniqelabel10')); - $w3->addLabels(array('Superuniqelabel9', 'Superuniqelabel10')); - - // get all the node's labels - $nLabels = $this->getNodeLabels($w1->id); - - $this->assertTrue(in_array('Superuniqelabel8', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel9', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel10', $nLabels)); - - // get all the node's labels - $nLabels = $this->getNodeLabels($w2->id); - $this->assertTrue(in_array('Superuniqelabel8', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel9', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel10', $nLabels)); - - // get all the node's labels - $nLabels = $this->getNodeLabels($w3->id); - $this->assertTrue(in_array('Superuniqelabel8', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel9', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel10', $nLabels)); - - //drop the label in sequence - $w1->dropLabels(array('Superuniqelabel8')); - $w2->dropLabels(array('Superuniqelabel8')); - $w3->dropLabels(array('Superuniqelabel8')); - - //drop the array of labels - $w1->dropLabels(array('Superuniqelabel9', 'Superuniqelabel10')); - $w2->dropLabels(array('Superuniqelabel9', 'Superuniqelabel10')); - $w3->dropLabels(array('Superuniqelabel9', 'Superuniqelabel10')); - - // get all the node's labels - $nLabels = $this->getNodeLabels($w1->id); - $this->assertFalse(in_array('Superuniqelabel8', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel9', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel10', $nLabels)); - - // get all the node's labels - $nLabels = $this->getNodeLabels($w2->id); - $this->assertFalse(in_array('Superuniqelabel8', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel9', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel10', $nLabels)); - - // get all the node's labels - $nLabels = $this->getNodeLabels($w3->id); - $this->assertFalse(in_array('Superuniqelabel8', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel9', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel10', $nLabels)); - } - - public function testAddDropLabelsRepeatedlyOnModelsFoundById() - { - //create a new model object - $w1 = new Labelwiz([ - 'fiz' => 'foo6', - 'biz' => 'boo6', - 'triz' => 'troo6', - ]); - $this->assertTrue($w1->save()); - - //create a new model object - $w2 = new Labelwiz([ - 'fiz' => 'foo7', - 'biz' => 'boo7', - 'triz' => 'troo7', - ]); - $this->assertTrue($w2->save()); - - //create a new model object - $w3 = new Labelwiz([ - 'fiz' => 'foo8', - 'biz' => 'boo8', - 'triz' => 'troo8', - ]); - $this->assertTrue($w3->save()); - - $f1 = Labelwiz::find($w1->id); - $f2 = Labelwiz::find($w2->id); - $f3 = Labelwiz::find($w3->id); - - //add the label in sequence - $f1->addLabels(array('Superuniqelabel11')); - $f2->addLabels(array('Superuniqelabel11')); - $f3->addLabels(array('Superuniqelabel11')); - - //add the array of labels - $f1->addLabels(array('Superuniqelabel12', 'Superuniqelabel13')); - $f2->addLabels(array('Superuniqelabel12', 'Superuniqelabel13')); - $f3->addLabels(array('Superuniqelabel12', 'Superuniqelabel13')); - - // get all the node's labels - $nLabels = $this->getNodeLabels($f1->id); - - $this->assertTrue(in_array('Superuniqelabel11', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel12', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel13', $nLabels)); - - // get all the node's labels - $nLabels = $this->getNodeLabels($f2->id); - $this->assertTrue(in_array('Superuniqelabel11', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel12', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel13', $nLabels)); - - // get all the node's labels - $nLabels = $this->getNodeLabels($f3->id); - $this->assertTrue(in_array('Superuniqelabel11', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel12', $nLabels)); - $this->assertTrue(in_array('Superuniqelabel13', $nLabels)); - - //drop the label in sequence - $f1->dropLabels(array('Superuniqelabel11')); - $f2->dropLabels(array('Superuniqelabel11')); - $f3->dropLabels(array('Superuniqelabel11')); - - //drop the array of labels - $f1->dropLabels(array('Superuniqelabel12', 'Superuniqelabel13')); - $f2->dropLabels(array('Superuniqelabel12', 'Superuniqelabel13')); - $f3->dropLabels(array('Superuniqelabel12', 'Superuniqelabel13')); - - // get all the node's labels - $nLabels = $this->getNodeLabels($f1->id); - $this->assertFalse(in_array('Superuniqelabel11', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel12', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel13', $nLabels)); - - // get all the node's labels - $nLabels = $this->getNodeLabels($f2->id); - $this->assertFalse(in_array('Superuniqelabel11', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel12', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel13', $nLabels)); - - // get all the node's labels - $nLabels = $this->getNodeLabels($f3->id); - $this->assertFalse(in_array('Superuniqelabel11', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel12', $nLabels)); - $this->assertFalse(in_array('Superuniqelabel13', $nLabels)); - } - - public function testAddDropLabelsOnRelated() - { - //create related nodes - $foo = Foo::createWith(['prop' => 'I am Foo'], ['bar' => ['prop' => 'I am Bar']]); - //$this->assertTrue($foo->save()); - - //now add labels on related node - $foo->bar->addLabels(['SpecialLabel1']); - $foo->bar->addLabels(['SpecialLabel2', 'SpecialLabel3', 'SpecialLabel4']); - - //get the Node using Everyman lib - $nLabels = $this->getNodeLabels($foo->bar->id); - $this->assertTrue(in_array('SpecialLabel1', $nLabels)); - $this->assertTrue(in_array('SpecialLabel2', $nLabels)); - $this->assertTrue(in_array('SpecialLabel3', $nLabels)); - $this->assertTrue(in_array('SpecialLabel4', $nLabels)); - - //now drop one label on related node - $foo->bar->dropLabels(['SpecialLabel1']); - - //get the Node using Everyman lib - $nLabels = $this->getNodeLabels($foo->bar->id); - $this->assertFalse(in_array('SpecialLabel1', $nLabels)); - $this->assertTrue(in_array('SpecialLabel2', $nLabels)); - $this->assertTrue(in_array('SpecialLabel3', $nLabels)); - $this->assertTrue(in_array('SpecialLabel4', $nLabels)); - - //now drop anotherlabel on related node - $foo->bar->dropLabels(['SpecialLabel2']); - - //get the Node using Everyman lib - $nLabels = $this->getNodeLabels($foo->bar->id); - $this->assertFalse(in_array('SpecialLabel1', $nLabels)); - $this->assertFalse(in_array('SpecialLabel2', $nLabels)); - $this->assertTrue(in_array('SpecialLabel3', $nLabels)); - $this->assertTrue(in_array('SpecialLabel4', $nLabels)); - - //now drop remaining labels on related node - $foo->bar->dropLabels(['SpecialLabel3', 'SpecialLabel4']); - - //get the Node using Everyman lib - $nLabels = $this->getNodeLabels($foo->bar->id); - $this->assertFalse(in_array('SpecialLabel1', $nLabels)); - $this->assertFalse(in_array('SpecialLabel2', $nLabels)); - $this->assertFalse(in_array('SpecialLabel3', $nLabels)); - $this->assertFalse(in_array('SpecialLabel4', $nLabels)); - } - - public function testDroppingLabels() - { - $w1 = new Labelwiz([ - 'fiz' => 'foo6', - 'biz' => 'boo6', - 'triz' => 'troo6', - ]); - $this->assertTrue($w1->save()); - - $id = $w1->id; - - //now drop the main label Labelwiz - $w1->dropLabels(['Labelwiz']); - - $nLabels = $this->getNodeLabels($id); - $this->assertFalse(in_array('Labelwiz', $nLabels)); - - //now find by id should NOT work on this id using Labelwiz model - $this->assertNull(Labelwiz::find($id)); - } -} diff --git a/tests/functional/AggregateTest.php b/tests/functional/AggregateTest.php deleted file mode 100644 index 39f0c814..00000000 --- a/tests/functional/AggregateTest.php +++ /dev/null @@ -1,319 +0,0 @@ -query = new Builder((new User())->getConnection(), new CypherGrammar()); - $this->query->from = 'User'; - } - - public function testCount() - { - User::create([]); - $this->assertEquals(1, $this->query->count()); - User::create([]); - $this->assertEquals(2, $this->query->count()); - User::create([]); - $this->assertEquals(3, $this->query->count()); - - User::create(['logins' => 10]); - $this->assertEquals(1, $this->query->count('logins')); - - User::create(['points' => 200]); - $this->assertEquals(1, $this->query->count('points')); - } - - public function testCountWithQuery() - { - User::create(['email' => 'foo@mail.net', 'points' => 2]); - User::create(['email' => 'bar@mail.net', 'points' => 2]); - // we need a fresh query every time so that we make sure we're not reusing the same - // one over and over which ends up with irreliable results. - $query = new Builder((new User())->getConnection(), new CypherGrammar()); - $query->from = 'User'; - $query->where('email', 'foo@mail.net'); - $this->assertEquals(1, $query->count()); - - $query = new Builder((new User())->getConnection(), new CypherGrammar()); - $query->from = 'User'; - $query->where('email', 'bar@mail.net'); - $this->assertEquals(1, $query->count()); - - $query = new Builder((new User())->getConnection(), new CypherGrammar()); - $query->from = 'User'; - $query->where('points', 2); - $this->assertEquals(2, $query->count()); - } - - public function testCountDistinct() - { - User::create(['logins' => 1]); - User::create(['logins' => 2]); - User::create(['logins' => 2]); - User::create(['logins' => 3]); - User::create(['logins' => 3]); - User::create(['logins' => 4]); - User::create(['logins' => 4]); - - $this->assertEquals(4, $this->query->countDistinct('logins')); - } - - public function testCountDistinctWithQuery() - { - User::create(['logins' => 1]); - User::create(['logins' => 2]); - User::create(['logins' => 2]); - User::create(['logins' => 3]); - User::create(['logins' => 3]); - User::create(['logins' => 4]); - User::create(['logins' => 4]); - - $this->query->where('logins', '>', 2); - $this->assertEquals(2, $this->query->countDistinct('logins')); - } - - public function testMax() - { - User::create(['logins' => 10, 'points' => 1]); - User::create(['logins' => 11, 'points' => 4]); - User::create(['logins' => 12, 'points' => 2]); - - $this->assertEquals(12, $this->query->max('logins')); - $this->assertEquals(4, $this->query->max('points')); - } - - public function testMaxWithQuery() - { - User::create(['logins' => 10, 'points' => 1]); - User::create(['logins' => 11, 'points' => 2]); - User::create(['logins' => 12, 'points' => 4]); - - $this->query->where('points', '<', 4); - $this->assertEquals(11, $this->query->max('logins')); - - $query = new Builder((new User())->getConnection(), new CypherGrammar()); - $query->from = 'User'; - $query->where('points', '<', 4); - $this->assertEquals(2, $query->max('points')); - } - - public function testMin() - { - User::create(['logins' => 10, 'points' => 1]); - User::create(['logins' => 11, 'points' => 4]); - User::create(['logins' => 12, 'points' => 2]); - - $this->assertEquals(10, $this->query->min('logins')); - $this->assertEquals(1, $this->query->min('points')); - } - - public function testMinWithQuery() - { - User::create(['logins' => 10, 'points' => 1]); - User::create(['logins' => 11, 'points' => 4]); - User::create(['logins' => 12, 'points' => 2]); - - $this->query->where('points', '>', 1); - $this->assertEquals(11, $this->query->min('logins')); - $this->assertEquals(2, $this->query->min('points')); - } - - public function testAvg() - { - User::create(['logins' => 10, 'points' => 1]); - User::create(['logins' => 11, 'points' => 4]); - User::create(['logins' => 12, 'points' => 2]); - - $this->assertEquals(11, $this->query->avg('logins')); - $this->assertEquals(2.3333333333333335, $this->query->avg('points')); - } - - public function testAvgWithQuery() - { - User::create(['logins' => 10, 'points' => 1]); - User::create(['logins' => 11, 'points' => 4]); - User::create(['logins' => 12, 'points' => 2]); - - $this->query->where('points', '>', 1); - - $this->assertEquals(11.5, $this->query->avg('logins')); - $this->assertEquals(3, $this->query->avg('points')); - } - - public function testSum() - { - User::create(['logins' => 10, 'points' => 1]); - User::create(['logins' => 11, 'points' => 4]); - User::create(['logins' => 12, 'points' => 2]); - - $this->assertEquals(33, $this->query->sum('logins')); - $this->assertEquals(7, $this->query->sum('points')); - } - - public function testSumWithQuery() - { - User::create(['logins' => 10, 'points' => 1]); - User::create(['logins' => 11, 'points' => 4]); - User::create(['logins' => 12, 'points' => 2]); - - $this->query->where('points', '>', 1); - $this->assertEquals(23, $this->query->sum('logins')); - $this->assertEquals(6, $this->query->sum('points')); - } - - public function testPercentileDisc() - { - User::create(['logins' => 10, 'points' => 1]); - User::create(['logins' => 11, 'points' => 4]); - User::create(['logins' => 12, 'points' => 2]); - - $this->assertEquals(10, $this->query->percentileDisc('logins')); - $this->assertEquals(11, $this->query->percentileDisc('logins', 0.5)); - $this->assertEquals(12, $this->query->percentileDisc('logins', 1)); - - $this->assertEquals(1, $this->query->percentileDisc('points')); - $this->assertEquals(2, $this->query->percentileDisc('points', 0.6)); - $this->assertEquals(4, $this->query->percentileDisc('points', 0.9)); - } - - public function testPercentileDiscWithQuery() - { - User::create(['logins' => 10, 'points' => 1]); - User::create(['logins' => 11, 'points' => 4]); - User::create(['logins' => 12, 'points' => 2]); - - $this->query->where('points', '>', 1); - $this->assertEquals(11, $this->query->percentileDisc('logins')); - $this->assertEquals(11, $this->query->percentileDisc('logins', 0.5)); - $this->assertEquals(12, $this->query->percentileDisc('logins', 1)); - - $this->assertEquals(2, $this->query->percentileDisc('points')); - $this->assertEquals(4, $this->query->percentileDisc('points', 0.6)); - $this->assertEquals(4, $this->query->percentileDisc('points', 0.9)); - } - - public function testPercentileCont() - { - User::create(['logins' => 10, 'points' => 1]); - User::create(['logins' => 11, 'points' => 4]); - User::create(['logins' => 12, 'points' => 2]); - - $this->assertEquals(10, $this->query->percentileCont('logins'), 0.2); - $this->assertEquals(10.800000000000001, $this->query->percentileCont('logins', 0.4)); - $this->assertEquals(11.800000000000001, $this->query->percentileCont('logins', 0.9)); - - $this->assertEquals(1, $this->query->percentileCont('points'), 0.3); - $this->assertEquals(2.3999999999999999, $this->query->percentileCont('points', 0.6)); - $this->assertEquals(3.6000000000000001, $this->query->percentileCont('points', 0.9)); - } - - public function testPercentileContWithQuery() - { - User::create(['logins' => 10, 'points' => 1]); - User::create(['logins' => 11, 'points' => 4]); - User::create(['logins' => 12, 'points' => 2]); - - $this->query->where('points', '<', 4); - $this->assertEquals(10.4, $this->query->percentileCont('logins', 0.2)); - $this->assertEquals(10.8, $this->query->percentileCont('logins', 0.4)); - $this->assertEquals(11.8, $this->query->percentileCont('logins', 0.9)); - - $this->assertEquals(1.2999999999999998, $this->query->percentileCont('points', 0.3)); - $this->assertEquals(1.6, $this->query->percentileCont('points', 0.6)); - $this->assertEquals(1.8999999999999999, $this->query->percentileCont('points', 0.9)); - } - - public function testStdev() - { - User::create(['logins' => 33, 'points' => 1]); - User::create(['logins' => 44, 'points' => 4]); - User::create(['logins' => 55, 'points' => 2]); - - $this->assertEquals(11, $this->query->stdev('logins')); - $this->assertEquals(1.5275252316519, $this->query->stdev('points')); - } - - public function testStdevWithQuery() - { - User::create(['logins' => 33, 'points' => 1]); - User::create(['logins' => 44, 'points' => 4]); - User::create(['logins' => 55, 'points' => 2]); - - $this->query->where('points', '>', 1); - $this->assertEquals(7.778174593052, $this->query->stdev('logins')); - $this->assertEquals(1.4142135623731, $this->query->stdev('points')); - } - - public function testStdevp() - { - User::create(['logins' => 33, 'points' => 1]); - User::create(['logins' => 44, 'points' => 4]); - User::create(['logins' => 55, 'points' => 2]); - - $this->assertEquals(8.981462390205, $this->query->stdevp('logins')); - $this->assertEquals(1.2472191289246, $this->query->stdevp('points')); - } - - public function testStdevpWithQuery() - { - User::create(['logins' => 33, 'points' => 1]); - User::create(['logins' => 44, 'points' => 4]); - User::create(['logins' => 55, 'points' => 2]); - - $this->query->where('points', '>', 1); - $this->assertEquals(5.5, $this->query->stdevp('logins')); - $this->assertEquals(1, $this->query->stdevp('points')); - } - - public function testCollect() - { - User::create(['logins' => 33, 'points' => 1]); - User::create(['logins' => 44, 'points' => 4]); - User::create(['logins' => 55, 'points' => 2]); - - $logins = $this->query->collect('logins'); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Collection', $logins); - $this->assertEquals(3, count($logins)); - $this->assertContains(33, $logins); - $this->assertContains(44, $logins); - $this->assertContains(55, $logins); - - $points = $this->query->collect('points'); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Collection', $points); - $this->assertEquals(3, count($points)); - $this->assertContains(1, $points); - $this->assertContains(4, $points); - $this->assertContains(2, $points); - } - - public function testCollectWithQuery() - { - User::create(['logins' => 33, 'points' => 1]); - User::create(['logins' => 44, 'points' => 4]); - User::create(['logins' => 55, 'points' => 2]); - - $logins = $this->query->where('points', '>', 1)->collect('logins'); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Collection', $logins); - - $this->assertEquals(2, count($logins)); - $this->assertContains(44, $logins); - $this->assertContains(55, $logins); - } -} - -class User extends Model -{ - protected $label = 'User'; - - protected $fillable = ['logins', 'points', 'email']; -} diff --git a/tests/functional/BelongsToManyRelationTest.php b/tests/functional/BelongsToManyRelationTest.php deleted file mode 100644 index 3e5c7c4e..00000000 --- a/tests/functional/BelongsToManyRelationTest.php +++ /dev/null @@ -1,443 +0,0 @@ -hasMany('Vinelab\NeoEloquent\Tests\Functional\Relations\BelongsToMany\Role', 'HAS_ROLE'); - } -} - -class Role extends Model -{ - protected $label = 'Role'; - - protected $fillable = ['title']; - - public function users() - { - return $this->belongsToMany('Vinelab\NeoEloquent\Tests\Functional\Relations\BelongsToMany\User', 'HAS_ROLE'); - } -} - -class BelongsToManyRelationTest extends TestCase -{ - public function tearDown(): void - { - M::close(); - - $users = User::all(); - $users->each(function ($u) { $u->delete(); }); - - $roles = Role::all(); - $roles->each(function ($r) { $r->delete(); }); - - parent::tearDown(); - } - - public function setUp(): void - { - parent::setUp(); - - $resolver = M::mock('Illuminate\Database\ConnectionResolverInterface'); - $resolver->shouldReceive('connection')->andReturn($this->getConnectionWithConfig('default')); - - User::setConnectionResolver($resolver); - Role::setConnectionResolver($resolver); - } - - public function testSavingRelatedBelongsToMany() - { - $user = User::create(['uuid' => '11213', 'name' => 'Creepy Dude']); - $role = new Role(['title' => 'Master']); - $relation = $user->roles()->save($role); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $relation); - $this->assertTrue($relation->exists()); - $this->assertGreaterThanOrEqual(0, $relation->id); - - $relation->delete(); - } - - public function testAttachingModelId() - { - $user = User::create(['uuid' => '4622', 'name' => 'Creepy Dude']); - $role = Role::create(['title' => 'Master']); - $relation = $user->roles()->attach($role->id); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $relation); - $this->assertTrue($relation->exists()); - $this->assertGreaterThan(0, $relation->id); - - $relation->delete(); - } - - public function testAttachingManyModelIds() - { - $user = User::create(['uuid' => '64753', 'name' => 'Creepy Dude']); - $master = Role::create(['title' => 'Master']); - $admin = Role::create(['title' => 'Admin']); - $editor = Role::create(['title' => 'Editor']); - - $relations = $user->roles()->attach([$master->id, $admin->id, $editor->id]); - - $this->assertCount(3, $relations->all()); - - $relations->each(function ($relation) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $relation); - $this->assertTrue($relation->exists()); - $this->assertGreaterThanOrEqual(0, $relation->id); - - $relation->delete(); - }); - } - - public function testAttachingModelInstance() - { - $user = User::create(['uuid' => '19583', 'name' => 'Creepy Dude']); - $role = Role::create(['title' => 'Master']); - - $relation = $user->roles()->attach($role); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $relation); - $this->assertTrue($relation->exists()); - $this->assertGreaterThanOrEqual(0, $relation->id); - - $retrieved = $user->roles()->edge($role); - $this->assertEquals($retrieved->toArray(), $relation->toArray()); - - $user->roles()->detach($role->id); - $this->assertNull($user->roles()->edge($role)); - } - - public function testAttachingManyModelInstances() - { - $user = User::create(['uuid' => '5346', 'name' => 'Creepy Dude']); - $master = new Role(['title' => 'Master']); - $admin = new Role(['title' => 'Admin']); - $editor = new Role(['title' => 'Editor']); - - $relations = $user->roles()->attach([$master, $admin, $editor]); - - $this->assertCount(3, $relations->all()); - - $relations->each(function ($relation) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $relation); - $this->assertTrue($relation->exists()); - $this->assertGreaterThan(0, $relation->id); - - $relation->delete(); - }); - } - - public function testAttachingNonExistingModelId() - { - $user = User::create(['uuid' => '3242', 'name' => 'Creepy Dude']); - $this->expectException(ModelNotFoundException::class); - $user->roles()->attach(10); - } - - public function testFindingBothEdges() - { - $user = User::create(['uuid' => '34525', 'name' => 'Creepy Dude']); - $role = Role::create(['title' => 'Master']); - $relation = $user->roles()->attach($role->id); - - $edgeOut = $user->roles()->edge($role); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $edgeOut); - $this->assertTrue($edgeOut->exists()); - $this->assertGreaterThan(0, $edgeOut->id); - - $edgeIn = $role->users()->edge($user); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeIn', $edgeIn); - $this->assertTrue($edgeIn->exists()); - $this->assertGreaterThan(0, $edgeIn->id); - - $relation->delete(); - } - - public function testDetachingModelById() - { - $user = User::create(['uuid' => '943543', 'name' => 'Creepy Dude']); - $role = Role::create(['title' => 'Master']); - - $relation = $user->roles()->attach($role->id); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $relation); - $this->assertTrue($relation->exists()); - $this->assertGreaterThan(0, $relation->id); - - $retrieved = $user->roles()->edge($role); - $this->assertEquals($retrieved->toArray(), $relation->toArray()); - - $user->roles()->detach($role->id); - $this->assertNull($user->roles()->edge($role)); - } - - public function testDetachingManyModelIds() - { - $user = User::create(['uuid' => '8363', 'name' => 'Creepy Dude']); - $master = Role::create(['title' => 'Master']); - $admin = Role::create(['title' => 'Admin']); - $editor = Role::create(['title' => 'Editor']); - - $relations = $user->roles()->attach([$master->id, $admin->id, $editor->id]); - - $this->assertCount(3, $relations->all()); - - // make sure they were successfully saved - $relations->each(function ($relation) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $relation); - $this->assertTrue($relation->exists()); - $this->assertGreaterThanOrEqual(0, $relation->id); - }); - - // Try retrieving them before detaching - $edges = $user->roles()->edges(); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Collection', $edges); - $this->assertCount(3, $edges->toArray()); - - $edges->each(function ($edge) { $edge->delete(); }); - } - - public function testSyncingModelIds() - { - $user = User::create(['uuid' => '25467', 'name' => 'Creepy Dude']); - $master = Role::create(['title' => 'Master']); - $admin = Role::create(['title' => 'Admin']); - $editor = Role::create(['title' => 'Editor']); - - $relation = $user->roles()->attach($master->getKey()); - - $user->roles()->sync([$admin->getKey(), $editor->getKey()]); - - $edges = $user->roles()->edges(); - - $edgesIds = array_map(function ($edge) { return $edge->getRelated()->getKey(); }, $edges->toArray()); - - $this->assertTrue(in_array($admin->getKey(), $edgesIds)); - $this->assertTrue(in_array($editor->getKey(), $edgesIds)); - $this->assertFalse(in_array($master->getKey(), $edgesIds)); - - foreach ($edges as $edge) { - $edge->delete(); - } - } - - public function testSyncingUpdatesModels() - { - $user = User::create(['uuid' => '14285', 'name' => 'Creepy Dude']); - $master = Role::create(['title' => 'Master']); - $admin = Role::create(['title' => 'Admin']); - $editor = Role::create(['title' => 'Editor']); - - $relation = $user->roles()->attach($master->id); - - $user->roles()->sync([$master->id, $admin->id, $editor->id]); - - $edges = $user->roles()->edges(); - - $edgesIds = array_map(function ($edge) { return $edge->getRelated()->getKey(); }, $edges->toArray()); - - $this->assertTrue(in_array($admin->id, $edgesIds)); - $this->assertTrue(in_array($editor->id, $edgesIds)); - $this->assertTrue(in_array($master->id, $edgesIds)); - - foreach ($edges as $edge) { - $edge->delete(); - } - } - - public function testSyncingWithAttributes() - { - $user = User::create(['uuid' => '83532', 'name' => 'Creepy Dude']); - $master = Role::create(['title' => 'Master']); - $admin = Role::create(['title' => 'Admin']); - $editor = Role::create(['title' => 'Editor']); - - $relation = $user->roles()->attach($master->id); - - $user->roles()->sync([ - $master->id => ['type' => 'Master'], - $admin->id => ['type' => 'Admin'], - $editor->id => ['type' => 'Editor'], - ]); - - $edges = $user->roles()->edges(); - - $edgesIds = array_map(function ($edge) { return $edge->getRelated()->getKey(); }, $edges->toArray()); - - // count the times that $master->id exists, it it were more than 1 then the relationship hasn't been updated, - // instead it was duplicated - $count = array_count_values((array) $master->id); - - $this->assertEquals(1, $count[$master->id]); - $this->assertTrue(in_array($admin->id, $edgesIds)); - $this->assertTrue(in_array($editor->id, $edgesIds)); - $this->assertTrue(in_array($master->id, $edgesIds)); - - $expectedEdgesTypes = array('Editor', 'Admin', 'Master'); - - foreach ($edges as $key => $edge) { - $attributes = $edge->toArray(); - $this->assertArrayHasKey('type', $attributes); - $this->assertTrue(in_array($edge->type, $expectedEdgesTypes)); - $index = array_search($edge->type, $expectedEdgesTypes); - unset($expectedEdgesTypes[$index]); - $edge->delete(); - } - } - - public function testDynamicLoadingBelongsToManyRelatedModels() - { - $user = User::create(['uuid' => '67887', 'name' => 'Creepy Dude']); - $master = Role::create(['title' => 'Master']); - $admin = Role::create(['title' => 'Admin']); - - $user->roles()->attach([$master, $admin]); - - foreach ($user->roles as $role) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Relations\BelongsToMany\Role', $role); - $this->assertTrue($role->exists); - $this->assertGreaterThan(0, $role->id); - } - - $user->roles()->edges()->each(function ($edge) { $edge->delete(); }); - } - - public function testEagerLoadingBelongsToMany() - { - $user = User::create(['uuid' => '44352', 'name' => 'Creepy Dude']); - $master = Role::create(['title' => 'Master']); - $admin = Role::create(['title' => 'Admin']); - $editor = Role::create(['title' => 'Editor']); - - $edges = $user->roles()->attach([$master, $admin, $editor]); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Collection', $edges); - - $creep = User::with('roles')->find($user->getKey()); - $relations = $creep->getRelations(); - - $this->assertArrayHasKey('roles', $relations); - $this->assertCount(3, $relations['roles']); - - $edges->each(function ($relation) { $relation->delete(); }); - } - - /** - * Regression for issue #120. - * - * @see https://github.com/Vinelab/NeoEloquent/issues/120 - */ - public function testDeletingBelongsToManyRelation() - { - $user = User::create(['uuid' => '34113', 'name' => 'Creepy Dude']); - $master = Role::create(['title' => 'Master']); - $admin = Role::create(['title' => 'Admin']); - $editor = Role::create(['title' => 'Editor']); - - $edges = $user->roles()->attach([$master, $admin, $editor]); - - $fetched = User::find($user->getKey()); - $this->assertEquals(3, count($user->roles), 'relations created successfully'); - - $deleted = $fetched->roles()->delete(); - $this->assertTrue((bool) $deleted); - - $again = User::find($user->getKey()); - $this->assertEquals(0, count($again->roles)); - - // roles should've been deleted too. - $masterDeleted = Role::where('title', 'Master')->first(); - $this->assertNull($masterDeleted); - - $adminDeleted = Role::where('title', 'Admin')->first(); - $this->assertNull($adminDeleted); - - $editorDeleted = Role::where('title', 'Edmin')->first(); - $this->assertNull($editorDeleted); - } - - /** - * Regression for issue #120. - * - * @see https://github.com/Vinelab/NeoEloquent/issues/120 - */ - public function testDeletingBelongsToManyRelationKeepingEndModels() - { - $user = User::create(['uuid' => '84633', 'name' => 'Creepy Dude']); - $master = Role::create(['title' => 'Master']); - $admin = Role::create(['title' => 'Admin']); - $editor = Role::create(['title' => 'Editor']); - - $edges = $user->roles()->attach([$master, $admin, $editor]); - - $fetched = User::find($user->getKey()); - $this->assertEquals(3, count($user->roles), 'relations created successfully'); - - $deleted = $fetched->roles()->delete(true); - $this->assertTrue((bool) $deleted); - - $again = User::find($user->getKey()); - $this->assertEquals(0, count($again->roles)); - - // roles should've been deleted too. - $masterDeleted = Role::find($master->getKey()); - $this->assertEquals($master->toArray(), $masterDeleted->toArray()); - - $adminDeleted = Role::find($admin->getKey()); - $this->assertEquals($admin->toArray(), $adminDeleted->toArray()); - - $editorDeleted = Role::find($editor->getKey()); - $this->assertEquals($editor->toArray(), $editorDeleted->toArray()); - } - - /** - * Regression for issue #120. - * - * @see https://github.com/Vinelab/NeoEloquent/issues/120 - */ - public function testDeletingModelBelongsToManyWithWhereHasRelation() - { - $user = User::create(['uuid' => '54556', 'name' => 'Creepy Dude']); - $master = Role::create(['title' => 'Master']); - $admin = Role::create(['title' => 'Admin']); - $editor = Role::create(['title' => 'Editor']); - - $edges = $user->roles()->attach([$master, $admin, $editor]); - - $fetched = User::find($user->getKey()); - $this->assertEquals(3, count($user->roles), 'relations created successfully'); - - $deleted = $fetched->whereHas('roles', function ($q) { - $q->where('title', 'Master'); - })->delete(); - - $this->assertTrue((bool) $deleted); - - $again = User::find($user->getKey()); - $this->assertNull($again); - - // roles should've been deleted too. - $masterDeleted = Role::find($master->getKey()); - $this->assertEquals($master->toArray(), $masterDeleted->toArray()); - - $adminDeleted = Role::find($admin->getKey()); - $this->assertEquals($admin->toArray(), $adminDeleted->toArray()); - - $editorDeleted = Role::find($editor->getKey()); - $this->assertEquals($editor->toArray(), $editorDeleted->toArray()); - } -} diff --git a/tests/functional/BelongsToRelationTest.php b/tests/functional/BelongsToRelationTest.php deleted file mode 100644 index 026faf2f..00000000 --- a/tests/functional/BelongsToRelationTest.php +++ /dev/null @@ -1,163 +0,0 @@ -belongsTo('Vinelab\NeoEloquent\Tests\Functional\Relations\BelongsTo\User', 'LOCATED_AT'); - } -} - -class BelongsToRelationTest extends TestCase -{ - public function tearDown(): void - { - M::close(); - - $users = User::all(); - $users->each(function ($u) { $u->delete(); }); - - $locs = Location::all(); - $locs->each(function ($l) { $l->delete(); }); - - parent::tearDown(); - } - - public function setUp(): void - { - parent::setUp(); - - $resolver = M::mock('Illuminate\Database\ConnectionResolverInterface'); - $resolver->shouldReceive('connection')->andReturn($this->getConnectionWithConfig('default')); - - User::setConnectionResolver($resolver); - Location::setConnectionResolver($resolver); - } - - public function testDynamicLoadingBelongsTo() - { - $location = Location::create(['lat' => 89765, 'long' => -876521234, 'country' => 'The Netherlands', 'city' => 'Amsterdam']); - $user = User::create(['name' => 'Daughter', 'alias' => 'daughter']); - $relation = $location->user()->associate($user); - - $fetched = Location::first(); - $this->assertEquals($user->toArray(), $fetched->user->toArray()); - $relation->delete(); - } - - public function testDynamicLoadingBelongsToFromFoundRecord() - { - $location = Location::create(['lat' => 89765, 'long' => -876521234, 'country' => 'The Netherlands', 'city' => 'Amsterdam']); - $user = User::create(['name' => 'Daughter', 'alias' => 'daughter']); - $relation = $location->user()->associate($user); - - $found = Location::find($location->id); - - $this->assertEquals($user->toArray(), $found->user->toArray()); - $this->assertTrue($relation->delete()); - } - - public function testEagerLoadingBelongsTo() - { - $location = Location::create(['lat' => 89765, 'long' => -876521234, 'country' => 'The Netherlands', 'city' => 'Amsterdam']); - $user = User::create(['name' => 'Daughter', 'alias' => 'daughter']); - $relation = $location->user()->associate($user); - - $found = Location::with('user')->find($location->id); - $relations = $found->getRelations(); - - $this->assertArrayHasKey('user', $relations); - $this->assertEquals($user->toArray(), $relations['user']->toArray()); - $this->assertTrue($relation->delete()); - } - - public function testAssociatingBelongingModel() - { - $location = Location::create(['lat' => 89765, 'long' => -876521234, 'country' => 'The Netherlands', 'city' => 'Amsterdam']); - $user = User::create(['name' => 'Daughter', 'alias' => 'daughter']); - $relation = $location->user()->associate($user); - - $this->assertInstanceOf('Carbon\Carbon', $relation->created_at, 'make sure we set the created_at timestamp'); - $this->assertInstanceOf('Carbon\Carbon', $relation->updated_at, 'make sure we set the updated_at timestamp'); - $this->assertArrayHasKey('user', $location->getRelations(), 'make sure the user has been set as relation in the model'); - $this->assertArrayHasKey('user', $location->toArray(), 'make sure it is also returned when dealing with the model'); - $this->assertEquals($location->user->toArray(), $user->toArray()); - - // Let's retrieve it to make sure that NeoEloquent is not lying about it. - $saved = Location::find($location->id); - $this->assertEquals($user->toArray(), $saved->user->toArray()); - - // delete the relation and make sure it was deleted - // so that we can delete the nodes when cleaning up. - $this->assertTrue($relation->delete()); - } - - public function testRetrievingAssociationFromParentModel() - { - $location = Location::create(['lat' => 52.3735291, 'long' => 4.886257, 'country' => 'The Netherlands', 'city' => 'Amsterdam']); - $user = User::create(['name' => 'Daughter', 'alias' => 'daughter']); - - $relation = $location->user()->associate($user); - $relation->since = 1966; - $this->assertTrue($relation->save()); - - $retrieved = $location->user()->edge($location->user); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeIn', $retrieved); - $this->assertEquals($retrieved->since, 1966); - - $this->assertTrue($retrieved->delete()); - } - - public function testSavingMultipleAssociationsKeepsOnlyTheLastOne() - { - $location = Location::create(['lat' => 52.3735291, 'long' => 4.886257, 'country' => 'The Netherlands']); - $van = User::create(['name' => 'Van Gogh', 'alias' => 'vangogh']); - - $relation = $location->user()->associate($van); - $relation->since = 1890; - $this->assertTrue($relation->save()); - - $jan = User::create(['name' => 'Jan Steen', 'alias' => 'jansteen']); - $cheating = $location->user()->associate($jan); - - $withVan = $location->user()->edge($van); - $this->assertNull($withVan); - - $withJan = $location->user()->edge($jan); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeIn', $withJan); - $this->assertTrue($withJan->delete()); - } - - public function testFindingEdgeWithNoSpecifiedModel() - { - $location = Location::create(['lat' => 52.3735291, 'long' => 4.886257, 'country' => 'The Netherlands', 'city' => 'Amsterdam']); - $user = User::create(['name' => 'Daughter', 'alias' => 'daughter']); - - $relation = $location->user()->associate($user); - $relation->since = 1966; - $this->assertTrue($relation->save()); - - $retrieved = $location->user()->edge(); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeIn', $retrieved); - $this->assertEquals($relation->id, $retrieved->id); - $this->assertEquals($relation->toArray(), $retrieved->toArray()); - $this->assertTrue($relation->delete()); - } -} diff --git a/tests/functional/HasManyRelationTest.php b/tests/functional/HasManyRelationTest.php deleted file mode 100644 index 1545693a..00000000 --- a/tests/functional/HasManyRelationTest.php +++ /dev/null @@ -1,369 +0,0 @@ -hasMany('Vinelab\NeoEloquent\Tests\Functional\Relations\HasMany\Book', 'WROTE'); - } -} - -class HasManyRelationTest extends TestCase -{ - public function tearDown(): void - { - M::close(); - - parent::tearDown(); - } - - public function setUp(): void - { - parent::setUp(); - - $resolver = M::mock('Illuminate\Database\ConnectionResolverInterface'); - $resolver->shouldReceive('connection')->andReturn($this->getConnectionWithConfig('default')); - - Author::setConnectionResolver($resolver); - Book::setConnectionResolver($resolver); - } - - public function testSavingSingleAndDynamicLoading() - { - $author = Author::create(['name' => 'George R. R. Martin']); - $got = new Book(['title' => 'A Game of Thrones', 'pages' => '704', 'release_date' => 'August 1996']); - $cok = new Book(['title' => 'A Clash of Kings', 'pages' => '768', 'release_date' => 'February 1999']); - $writtenGot = $author->books()->save($got, ['ratings' => '123']); - $writtenCok = $author->books()->save($cok, ['chapters' => 70]); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $writtenGot); - $this->assertTrue($writtenGot->exists()); - $this->assertGreaterThanOrEqual(0, $writtenGot->id); - $this->assertNotNull($writtenGot->created_at); - $this->assertNotNull($writtenGot->updated_at); - $this->assertEquals($writtenGot->ratings, 123); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $writtenCok); - $this->assertTrue($writtenCok->exists()); - $this->assertGreaterThan(0, $writtenCok->id); - $this->assertNotNull($writtenCok->created_at); - $this->assertNotNull($writtenCok->updated_at); - $this->assertEquals($writtenCok->chapters, 70); - - $books = $author->books; - - $expectedBooks = [ - $got->title => $got->toArray(), - $cok->title => $cok->toArray(), - ]; - - $this->assertCount(2, $books->toArray()); - - foreach ($books as $book) { - $this->assertEquals($expectedBooks[$book->title], $book->toArray()); - unset($expectedBooks[$book->title]); - } - - $writtenGot->delete(); - $writtenCok->delete(); - } - - public function testSavingManyAndDynamicLoading() - { - $author = Author::create(['name' => 'George R. R. Martin']); - - $novel = [ - new Book([ - 'title' => 'A Game of Thrones', - 'pages' => 704, - 'release_date' => 'August 1996', - ]), - new Book([ - 'title' => 'A Clash of Kings', - 'pages' => 768, - 'release_date' => 'February 1999', - ]), - new Book([ - 'title' => 'A Storm of Swords', - 'pages' => 992, - 'release_date' => 'November 2000', - ]), - new Book([ - 'title' => 'A Feast for Crows', - 'pages' => 753, - 'release_date' => 'November 2005', - ]), - ]; - - $edges = $author->books()->saveMany($novel); - $this->assertCount(count($novel), $edges->toArray()); - - $books = $author->books->toArray(); - $this->assertCount(count($novel), $books); - - foreach ($edges as $key => $edge) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $edge); - $this->assertTrue($edge->exists()); - $this->assertGreaterThanOrEqual(0, $edge->id); - $this->assertNotNull($edge->created_at); - $this->assertNotNull($edge->updated_at); - $edge->delete(); - } - } - - public function testCreatingSingleRelatedModels() - { - $author = Author::create(['name' => 'George R. R. Martin']); - - $novel = [ - [ - 'title' => 'A Game of Thrones', - 'pages' => 704, - 'release_date' => 'August 1996', - ], - [ - 'title' => 'A Clash of Kings', - 'pages' => 768, - 'release_date' => 'February 1999', - ], - [ - 'title' => 'A Storm of Swords', - 'pages' => 992, - 'release_date' => 'November 2000', - ], - [ - 'title' => 'A Feast for Crows', - 'pages' => 753, - 'release_date' => 'November 2005', - ], - ]; - - foreach ($novel as $book) { - $edge = $author->books()->create($book, ['on' => $book['release_date']]); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $edge); - $this->assertTrue($edge->exists()); - $this->assertGreaterThan(0, $edge->id); - $this->assertNotNull($edge->created_at); - $this->assertNotNull($edge->updated_at); - $this->assertEquals($edge->on, $book['release_date']); - $edge->delete(); - } - } - - public function testCreatingManyRelatedModels() - { - $author = Author::create(['name' => 'George R. R. Martin']); - - $novel = [ - [ - 'title' => 'A Game of Thrones', - 'pages' => 704, - 'release_date' => 'August 1996', - ], - [ - 'title' => 'A Clash of Kings', - 'pages' => 768, - 'release_date' => 'February 1999', - ], - [ - 'title' => 'A Storm of Swords', - 'pages' => 992, - 'release_date' => 'November 2000', - ], - [ - 'title' => 'A Feast for Crows', - 'pages' => 753, - 'release_date' => 'November 2005', - ], - ]; - - $edges = $author->books()->createMany($novel); - - foreach ($edges as $edge) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $edge); - $this->assertTrue($edge->exists()); - $this->assertGreaterThanOrEqual(0, $edge->id); - $this->assertNotNull($edge->created_at); - $this->assertNotNull($edge->updated_at); - - $edge->delete(); - } - } - - public function testEagerLoadingHasMany() - { - $author = Author::create(['name' => 'George R. R. Martin']); - - $novel = [ - new Book([ - 'title' => 'A Game of Thrones', - 'pages' => 704, - 'release_date' => 'August 1996', - ]), - new Book([ - 'title' => 'A Clash of Kings', - 'pages' => 768, - 'release_date' => 'February 1999', - ]), - new Book([ - 'title' => 'A Storm of Swords', - 'pages' => 992, - 'release_date' => 'November 2000', - ]), - new Book([ - 'title' => 'A Feast for Crows', - 'pages' => 753, - 'release_date' => 'November 2005', - ]), - ]; - - $edges = $author->books()->saveMany($novel); - $this->assertCount(count($novel), $edges->toArray()); - - $author = Author::with('books')->find($author->id); - $relations = $author->getRelations(); - - $this->assertArrayHasKey('books', $relations); - $this->assertCount(count($novel), $relations['books']->toArray()); - - $booksIds = array_map(function ($book) { return $book->getKey(); }, $novel); - - foreach ($relations['books'] as $key => $book) { - $this->assertTrue(in_array($book->getKey(), $booksIds)); - $edge = $author->books()->edge($book); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $edge); - } - } - - public function testSavingManyRelationsWithRelationProperties() - { - $author = Author::create(['name' => 'George R. R. Martin']); - - $novel = [ - new Book([ - 'title' => 'A Game of Thrones', - 'pages' => 704, - 'release_date' => 'August 1996', - ]), - new Book([ - 'title' => 'A Clash of Kings', - 'pages' => 768, - 'release_date' => 'February 1999', - ]), - new Book([ - 'title' => 'A Storm of Swords', - 'pages' => 992, - 'release_date' => 'November 2000', - ]), - new Book([ - 'title' => 'A Feast for Crows', - 'pages' => 753, - 'release_date' => 'November 2005', - ]), - ]; - - $edges = $author->books()->saveMany($novel, ['novel' => true]); - $this->assertCount(count($novel), $edges->toArray()); - - foreach ($edges as $edge) { - $this->assertTrue($edge->novel); - $edge->delete(); - } - } - - public function testSyncingModelIds() - { - $author = Author::create(['name' => 'George R.R. Martin']); - $bk = Book::create(['title' => 'foo']); - $got = Book::create(['title' => 'A Game of Thrones', 'pages' => '704', 'release_date' => 'August 1996']); - $cok = Book::create(['title' => 'A Clash of Kings', 'pages' => '768', 'release_date' => 'February 1999']); - - $author->books()->attach($bk); - - $author->books()->sync([$got->id, $cok->id]); - - $edges = $author->books()->edges(); - - $edgesIds = array_map(function ($edge) { return $edge->getRelated()->getKey(); }, $edges->toArray()); - - $this->assertTrue(in_array($got->id, $edgesIds)); - $this->assertTrue(in_array($cok->id, $edgesIds)); - $this->assertFalse(in_array($bk->id, $edgesIds)); - } - - public function testSyncingWithIdsUpdatesModels() - { - $author = Author::create(['name' => 'George R.R. Martin']); - $got = Book::create(['title' => 'A Game of Thrones', 'pages' => '704', 'release_date' => 'August 1996']); - $cok = Book::create(['title' => 'A Clash of Kings', 'pages' => '768', 'release_date' => 'February 1999']); - $sos = Book::create(['title' => 'A Storm of Swords', 'pages' => 992, 'release_date' => 'November 2000']); - - $author->books()->attach($got); - - $author->books()->sync([$got->id, $cok->id, $sos->id]); - - $edges = $author->books()->edges(); - - $edgesIds = array_map(function ($edge) { return $edge->getRelated()->getKey(); }, $edges->toArray()); - - $this->assertTrue(in_array($got->id, $edgesIds)); - $this->assertTrue(in_array($cok->id, $edgesIds)); - $this->assertTrue(in_array($sos->id, $edgesIds)); - } - - public function testSyncingWithAttributes() - { - $author = Author::create(['name' => 'George R.R. Martin']); - $got = Book::create(['title' => 'A Game of Thrones', 'pages' => '704', 'release_date' => 'August 1996']); - $cok = Book::create(['title' => 'A Clash of Kings', 'pages' => '768', 'release_date' => 'February 1999']); - $sos = Book::create(['title' => 'A Storm of Swords', 'pages' => 992, 'release_date' => 'November 2000']); - - $author->books()->attach($got); - - $author->books()->sync([ - $got->id => ['series' => 'Game'], - $cok->id => ['series' => 'Clash'], - $sos->id => ['series' => 'Storm'], - ]); - - $edges = $author->books()->edges(); - - $edgesIds = array_map(function ($edge) { return $edge->getRelated()->getKey(); }, $edges->toArray()); - - $count = array_count_values((array) $got->id); - - $this->assertEquals(1, $count[$got->id]); - $this->assertTrue(in_array($cok->id, $edgesIds)); - $this->assertTrue(in_array($sos->id, $edgesIds)); - $this->assertTrue(in_array($got->id, $edgesIds)); - - $expectedEdgesTypes = array('Storm', 'Clash', 'Game'); - - foreach ($edges as $key => $edge) { - $attributes = $edge->toArray(); - $this->assertArrayHasKey('series', $attributes); - $this->assertTrue(in_array($edge->series, $expectedEdgesTypes)); - $index = array_search($edge->series, $expectedEdgesTypes); - unset($expectedEdgesTypes[$index]); - $edge->delete(); - } - } -} diff --git a/tests/functional/HasOneRelationTest.php b/tests/functional/HasOneRelationTest.php deleted file mode 100644 index 8d299e04..00000000 --- a/tests/functional/HasOneRelationTest.php +++ /dev/null @@ -1,167 +0,0 @@ -hasOne('Vinelab\NeoEloquent\Tests\Functional\Relations\HasOne\Profile', 'PROFILE'); - } -} - -class Profile extends Model -{ - protected $label = 'Profile'; - - protected $fillable = ['guid', 'service']; -} - -class HasOneRelationTest extends TestCase -{ - public function tearDown(): void - { - M::close(); - - parent::tearDown(); - } - - public function setUp(): void - { - parent::setUp(); - - $resolver = M::mock('Illuminate\Database\ConnectionResolverInterface'); - $resolver->shouldReceive('connection')->andReturn($this->getConnectionWithConfig('default')); - - User::setConnectionResolver($resolver); - Profile::setConnectionResolver($resolver); - } - - public function testDynamicLoadingHasOne() - { - $user = User::create(['name' => 'Tests', 'email' => 'B']); - $profile = Profile::create(['guid' => uniqid(), 'service' => 'twitter']); - - $relation = $user->profile()->save($profile); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $relation); - $this->assertEquals($profile->toArray(), $user->profile->toArray()); - $this->assertTrue($relation->delete()); - } - - public function testDynamicLoadingHasOneFromFoundRecord() - { - $user = User::create(['name' => 'Tests', 'email' => 'B']); - $profile = Profile::create(['guid' => uniqid(), 'service' => 'twitter']); - - $relation = $user->profile()->save($profile); - - $found = User::find($user->id); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $relation); - $this->assertEquals($profile->toArray(), $found->profile->toArray()); - $this->assertTrue($relation->delete()); - } - - public function testEagerLoadingHasOne() - { - $user = User::create(['name' => 'Tests', 'email' => 'B']); - $profile = Profile::create(['guid' => uniqid(), 'service' => 'twitter']); - - $relation = $user->profile()->save($profile); - - $found = User::with('profile')->find($user->id); - $relations = $found->getRelations(); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $relation); - $this->assertArrayHasKey('profile', $relations); - $this->assertEquals($profile->toArray(), $relations['profile']->toArray()); - $this->assertTrue($relation->delete()); - } - - public function testSavingRelatedHasOneModel() - { - $user = User::create(['name' => 'Tests', 'email' => 'B']); - $profile = Profile::create(['guid' => uniqid(), 'service' => 'twitter']); - - $relation = $user->profile()->save($profile); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $relation); - - $this->assertInstanceOf('Carbon\Carbon', $relation->created_at, 'make sure we set the created_at timestamp'); - $this->assertInstanceOf('Carbon\Carbon', $relation->updated_at, 'make sure we set the updated_at timestamp'); - $this->assertEquals($user->profile->toArray(), $profile->toArray()); - - // Let's retrieve it to make sure that NeoEloquent is not lying about it. - $saved = User::find($user->id); - $this->assertEquals($profile->toArray(), $saved->profile->toArray()); - - // delete the relation and make sure it was deleted - // so that we can delete the nodes when cleaning up. - $this->assertTrue($relation->delete()); - } - - public function testRetrievingRelationWithAttributesSpecifyingEdgeModel() - { - $user = User::create(['name' => 'Tests', 'email' => 'B']); - $profile = Profile::create(['guid' => uniqid(), 'service' => 'twitter']); - - $relation = $user->profile()->save($profile); - - $relation->active = true; - - $this->assertTrue($relation->save()); - - $retrieved = $user->profile()->edge($profile); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $retrieved); - $this->assertTrue($retrieved->active); - $this->assertTrue($retrieved->delete()); - } - - public function testSavingMultipleRelationsKeepsOnlyTheLastOne() - { - $user = User::create(['name' => 'Tests', 'email' => 'B']); - $profile = Profile::create(['guid' => uniqid(), 'service' => 'twitter']); - - $relation = $user->profile()->save($profile); - $relation->use = 'casual'; - $this->assertTrue($relation->save()); - - $cv = Profile::create(['guid' => uniqid(), 'service' => 'linkedin']); - $linkedin = $user->profile()->save($cv); - $linkedin->use = 'official'; - $this->assertTrue($linkedin->save()); - - $withPr = $user->profile()->edge($profile); - $this->assertNull($withPr); - - $withCv = $user->profile()->edge($cv); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $withCv); - $this->assertEquals($withCv->use, 'official'); - $this->assertTrue($withCv->delete()); - } - - public function testFindingEdgeWithNoSpecifiedEdgeModel() - { - $user = User::create(['name' => 'Tests', 'email' => 'B']); - $profile = Profile::create(['guid' => uniqid(), 'service' => 'twitter']); - - $relation = $user->profile()->save($profile); - $relation->active = true; - $this->assertTrue($relation->save()); - - $retrieved = $user->profile()->edge(); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $retrieved); - $this->assertEquals($relation->id, $retrieved->id); - $this->assertEquals($relation->toArray(), $retrieved->toArray()); - $this->assertTrue($relation->delete()); - } -} diff --git a/tests/functional/ModelEventsTest.php b/tests/functional/ModelEventsTest.php deleted file mode 100644 index e9fa4a42..00000000 --- a/tests/functional/ModelEventsTest.php +++ /dev/null @@ -1,373 +0,0 @@ - 'a']); - - $obOne = OBOne::first(); - - $this->assertTrue($obOne->ob_creating_event); - $this->assertTrue($obOne->ob_created_event); - $this->assertTrue($obOne->ob_saving_event); - $this->assertTrue($obOne->ob_saved_event); - - // find for deletion - $obOne = OBOne::first(); - $obOne->delete(); - - $this->assertTrue($obOne->ob_deleting_event); - $this->assertTrue($obOne->ob_deleted_event); - - $obOne = OBOne::onlyTrashed()->first(); - $obOne->restore(); - - $this->assertTrue($obOne->ob_restoring_event); - $this->assertTrue($obOne->ob_restored_event); - } - - public function testDispatchedEventsChainSetOnBoot() - { - User::create(['name' => 'a']); - - $user = User::first(); - - $this->assertTrue($user->creating_event); - $this->assertTrue($user->created_event); - $this->assertTrue($user->saving_event); - $this->assertTrue($user->saved_event); - - // find for deletion - $user = User::first(); - $user->delete(); - - $this->assertTrue($user->deleting_event); - $this->assertTrue($user->deleted_event); - - $user = User::onlyTrashed()->first(); - $user->restore(); - - $this->assertTrue($user->restoring_event); - $this->assertTrue($user->restored_event); - } - - public function testCreateWithDispatchedEventsChainSetOnBoot() - { - User::createWith(['name' => 'a'], ['friends' => ['name' => 'b']]); - - $friend = Friend::first(); - - $this->assertTrue($friend->creating_event); - $this->assertTrue($friend->created_event); - $this->assertTrue($friend->saving_event); - $this->assertTrue($friend->saved_event); - } - - public function testCreateWithDispatchedEventsChainSetOnBootWithExistingRelationModel() - { - $friend = Friend::create(['name' => 'b']); - - $friend->creating_event = false; - $friend->created_event = false; - $friend->saving_event = false; - $friend->saved_event = false; - - $friend->save(); - - User::createWith(['name' => 'a'], ['friends' => $friend]); - - $this->assertNotTrue($friend->creating_event); - $this->assertNotTrue($friend->created_event); - $this->assertTrue($friend->saving_event); - $this->assertTrue($friend->saved_event); - } -} - -class User extends Model -{ - use SoftDeletes; - - protected $dates = ['deleted_at']; - - protected $label = 'User'; - - protected $fillable = [ - 'name', - 'creating_event', - 'created_event', - 'updating_event', - 'updated_event', - 'saving_event', - 'saved_event', - 'deleting_event', - 'deleted_event', - 'restoring_event', - 'restored_event', - ]; - - // Will hold the events and their callbacks - protected static $listenerStub = []; - - public static function boot() - { - // Mock a dispatcher - $dispatcher = M::mock('EventDispatcher'); - $dispatcher->shouldReceive('listen')->andReturnUsing(function ($event, $callback) { - static::$listenerStub[$event] = $callback; - }); - $dispatcher->shouldReceive('until')->andReturnUsing(function ($event, $model) { - if (isset(static::$listenerStub[$event])) { - call_user_func(static::$listenerStub[$event], $model); - } - }); - $dispatcher->shouldReceive('dispatch')->andReturnUsing(function ($event, $model) { - if (isset(static::$listenerStub[$event])) { - call_user_func(static::$listenerStub[$event], $model); - } - }); - - static::$dispatcher = $dispatcher; - - // boot up model - parent::boot(); - - self::creating(function ($user) { - $user->creating_event = true; - }); - - self::created(function ($user) { - $user->created_event = true; - $user->save(); - }); - - self::saving(function ($user) { - $user->saving_event = true; - }); - - self::saved(function ($user) { - if (!$user->saved_event) { - $user->saved_event = true; - $user->save(); - } - }); - - self::deleting(function ($user) { - $user->deleting_event = true; - }); - - self::deleted(function ($user) { - $user->deleted_event = true; - unset($user->id); - $user->save(); - }); - - self::restoring(function ($user) { - $user->restoring_event = true; - }); - - self::restored(function ($user) { - $user->restored_event = true; - $user->save(); - }); - } - - public function friends() - { - return $this->hasMany(Friend::class, 'friend'); - } -} - -class Friend extends Model -{ - protected $label = 'Friend'; - - protected $fillable = [ - 'name', - 'creating_event', - 'created_event', - 'updating_event', - 'updated_event', - 'saving_event', - 'saved_event', - ]; - - // Will hold the events and their callbacks - protected static $listenerStub = []; - - public static function boot() - { - // Mock a dispatcher - $dispatcher = M::mock('EventDispatcher'); - $dispatcher->shouldReceive('listen')->andReturnUsing(function ($event, $callback) { - static::$listenerStub[$event] = $callback; - }); - $dispatcher->shouldReceive('until')->andReturnUsing(function ($event, $model) { - if (isset(static::$listenerStub[$event])) { - call_user_func(static::$listenerStub[$event], $model); - } - }); - $dispatcher->shouldReceive('dispatch')->andReturnUsing(function ($event, $model) { - if (isset(static::$listenerStub[$event])) { - call_user_func(static::$listenerStub[$event], $model); - } - }); - - static::$dispatcher = $dispatcher; - - // boot up model - parent::boot(); - - self::creating(function ($friend) { - $friend->creating_event = true; - }); - - self::created(function ($friend) { - $friend->created_event = true; - $friend->save(); - }); - - self::saving(function ($friend) { - $friend->saving_event = true; - }); - - self::saved(function ($friend) { - if (!$friend->saved_event) { - $friend->saved_event = true; - $friend->save(); - } - }); - } - - public function user() - { - return $this->belongsTo(User::class, 'friend'); - } -} - -class OBOne extends Model -{ - use SoftDeletes; - - protected $dates = ['deleted_at']; - - protected $label = 'OBOne'; - - protected static $listenerStub = []; - - protected $fillable = [ - 'name', - 'ob_creating_event', - 'ob_created_event', - 'ob_updating_event', - 'ob_updated_event', - 'ob_saving_event', - 'ob_saved_event', - 'ob_deleting_event', - 'ob_deleted_event', - 'ob_restoring_event', - 'ob_restored_event', - ]; - - // We'll just cancel out the events that were put on - // the User model at boot time so that we make sure - // we're using the observer ones. - public static function boot() - { - parent::boot(); - - // Mock a dispatcher - $dispatcher = M::mock('OBEventDispatcher'); - $dispatcher->shouldReceive('listen')->andReturnUsing(function ($event, $callback) { - static::$listenerStub[$event] = $callback; - }); - $dispatcher->shouldReceive('until')->andReturnUsing(function ($event, $model) { - if (isset(static::$listenerStub[$event]) and strpos(static::$listenerStub[$event], '@') !== false) { - list($listener, $method) = explode('@', static::$listenerStub[$event]); - if (isset(static::$listenerStub[$event])) { - call_user_func([$listener, $method], $model); - } - } elseif (isset(static::$listenerStub[$event])) { - call_user_func(static::$listenerStub[$event], $model); - } - }); - $dispatcher->shouldReceive('dispatch')->andReturnUsing(function ($event, $model) { - if (isset(static::$listenerStub[$event]) and strpos(static::$listenerStub[$event], '@') !== false) { - list($listener, $method) = explode('@', static::$listenerStub[$event]); - if (isset(static::$listenerStub[$event])) { - call_user_func([$listener, $method], $model); - } - } elseif (isset(static::$listenerStub[$event])) { - call_user_func(static::$listenerStub[$event], $model); - } - }); - - static::$dispatcher = $dispatcher; - } -} - -class UserObserver -{ - public static function creating($ob) - { - $ob->ob_creating_event = true; - } - - public static function created($ob) - { - $ob->ob_created_event = true; - $ob->save(); - } - - public static function saving($ob) - { - $ob->ob_saving_event = true; - } - - public static function saved($ob) - { - if (!$ob->ob_saved_event) { - $ob->ob_saved_event = true; - $ob->save(); - } - } - - public static function deleting($ob) - { - $ob->ob_deleting_event = true; - } - - public static function deleted($ob) - { - $ob->ob_deleted_event = true; - unset($ob->id); - $ob->save(); - } - - public static function restoring($ob) - { - $ob->ob_restoring_event = true; - } - - public static function restored($ob) - { - $ob->ob_restored_event = true; - $ob->save(); - } -} - -OBOne::observe(new UserObserver()); diff --git a/tests/functional/OrdersAndLimitsTest.php b/tests/functional/OrdersAndLimitsTest.php deleted file mode 100644 index e76a6a68..00000000 --- a/tests/functional/OrdersAndLimitsTest.php +++ /dev/null @@ -1,70 +0,0 @@ -shouldReceive('connection')->andReturn($this->getConnectionWithConfig('default')); - Click::setConnectionResolver($resolver); - } - - public function tearDown(): void - { - M::close(); - - parent::tearDown(); - } - - public function testFetchingOrderedRecords() - { - $c1 = Click::create(['num' => 1]); - $c2 = Click::create(['num' => 2]); - $c3 = Click::create(['num' => 3]); - - $clicks = Click::orderBy('num', 'desc')->get(); - - $this->assertEquals(3, count($clicks)); - - $this->assertEquals($c3->toArray(), $clicks[0]->toArray()); - $this->assertEquals($c2->toArray(), $clicks[1]->toArray()); - $this->assertEquals($c1->toArray(), $clicks[2]->toArray()); - - $asc = Click::orderBy('num', 'asc')->get(); - - $this->assertEquals($c1->toArray(), $asc[0]->toArray()); - $this->assertEquals($c2->toArray(), $asc[1]->toArray()); - $this->assertEquals($c3->toArray(), $asc[2]->toArray()); - } - - public function testFetchingLimitedOrderedRecords() - { - $c1 = Click::create(['num' => 1]); - $c2 = Click::create(['num' => 2]); - $c3 = Click::create(['num' => 3]); - - $click = Click::orderBy('num', 'desc')->take(1)->get(); - $this->assertEquals(1, count($click)); - $this->assertEquals($c3->toArray(), $click[0]->toArray()); - - $another = Click::orderBy('num', 'asc')->take(2)->get(); - $this->assertEquals(2, count($another)); - $this->assertEquals($c1->toArray(), $another[0]->toArray()); - $this->assertEquals($c2->toArray(), $another[1]->toArray()); - } -} - -class Click extends Model -{ - protected $label = 'Click'; - - protected $fillable = ['num']; -} diff --git a/tests/functional/ParameterGroupingTest.php b/tests/functional/ParameterGroupingTest.php deleted file mode 100644 index d1eab3fd..00000000 --- a/tests/functional/ParameterGroupingTest.php +++ /dev/null @@ -1,71 +0,0 @@ -hasOne('Vinelab\NeoEloquent\Tests\Functional\ParameterGrouping\FacebookAccount', 'HAS_FACEBOOK_ACCOUNT'); - } -} - -class FacebookAccount extends Model -{ - protected $label = 'SocialAccount'; - protected $fillable = ['gender', 'age', 'interest']; -} - -class ParameterGroupingTest extends TestCase -{ - public function tearDown(): void - { - M::close(); - - parent::tearDown(); - } - - public function setUp(): void - { - parent::setUp(); - - $resolver = M::mock('Illuminate\Database\ConnectionResolverInterface'); - $resolver->shouldReceive('connection')->andReturn($this->getConnectionWithConfig('default')); - - User::setConnectionResolver($resolver); - FacebookAccount::setConnectionResolver($resolver); - } - - public function testNestedWhereClause() - { - $searchedUser = User::create(['name' => 'John Doe']); - $searchedUser->facebookAccount()->save(FacebookAccount::create([ - 'gender' => 'male', - 'age' => 20, - 'interest' => 'Dancing', - ])); - - $anotherUser = User::create(['name' => 'John Smith']); - $anotherUser->facebookAccount()->save(FacebookAccount::create([ - 'gender' => 'male', - 'age' => 30, - 'interest' => 'Music', - ])); - - $users = User::whereHas('facebookAccount', function ($query) { - $query->where('gender', 'male')->where(function ($query) { - $query->orWhere('age', '<', 24)->orWhere('interest', 'Entertainment'); - }); - })->get(); - - $this->assertCount(1, $users); - $this->assertEquals($searchedUser->name, $users->shift()->name); - } -} diff --git a/tests/functional/PolymorphicHyperMorphToTest.php b/tests/functional/PolymorphicHyperMorphToTest.php deleted file mode 100644 index d4ce8835..00000000 --- a/tests/functional/PolymorphicHyperMorphToTest.php +++ /dev/null @@ -1,707 +0,0 @@ -shouldReceive('connection')->andReturn($this->getConnectionWithConfig('default')); - - User::setConnectionResolver($resolver); - Post::setConnectionResolver($resolver); - Video::setConnectionResolver($resolver); - Comment::setConnectionResolver($resolver); - } - - public function testCreatingUserCommentOnPostAndVideo() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - // Comment on post and video - $postComment = $postCommentor->comments($post)->create(['text' => 'Please soooooon!']); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $postComment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $postComment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $postComment->right()); - $this->assertTrue($postComment->exists()); - - $videoComment = $videoCommentor->comments($video)->create(['text' => 'Haha, hilarious shit!']); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $videoComment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $videoComment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $videoComment->right()); - $this->assertTrue($videoComment->exists()); - - $this->assertNotEquals($postComment, $videoComment); - } - - public function testSavingUserCommentOnPostAndVideo() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - $commentOnPost = new Comment(['title' => 'Another Place', 'body' => 'To Go..']); - $commentOnVideo = new Comment(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Comment on post and video - $postComment = $postCommentor->comments($post)->save($commentOnPost); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $postComment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $postComment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $postComment->right()); - $this->assertTrue($postComment->exists()); - - $videoComment = $videoCommentor->comments($video)->save($commentOnVideo); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $videoComment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $videoComment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $videoComment->right()); - $this->assertTrue($videoComment->exists()); - - $this->assertNotEquals($postComment, $videoComment); - } - - public function testAttachingById() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - $commentOnPost = Comment::create(['text' => 'Another Place']); - $commentOnVideo = Comment::create(['text' => 'When We Meet']); - // Comment on post and video - $postComment = $postCommentor->comments($post)->attach($commentOnPost->id); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $postComment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $postComment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $postComment->right()); - $this->assertTrue($postComment->exists()); - - $videoComment = $videoCommentor->comments($video)->attach($commentOnVideo->id); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $videoComment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $videoComment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $videoComment->right()); - $this->assertTrue($videoComment->exists()); - - $this->assertNotEquals($postComment, $videoComment); - } - - public function testAttachingManyIds() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - $commentOnPost = Comment::create(['text' => 'Another Place']); - $anotherCommentOnPost = Comment::create(['text' => 'Here and there']); - $commentOnVideo = Comment::create(['text' => 'When We Meet']); - $anotherCommentOnVideo = Comment::create(['text' => 'That is good']); - - // Comment on post and video - $postComments = $postCommentor->comments($post)->attach([$commentOnPost->id, $anotherCommentOnPost->id]); - foreach ($postComments as $comment) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $comment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $comment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $comment->right()); - $this->assertTrue($comment->exists()); - } - - $videoComments = $videoCommentor->comments($video)->attach([$commentOnVideo->id, $anotherCommentOnVideo->id]); - foreach ($videoComments as $comment) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $comment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $comment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $comment->right()); - $this->assertTrue($comment->exists()); - } - - $this->assertNotEquals($postComments, $videoComments); - } - - public function testAttachingModelInstance() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - // Comment on post and video - $commentOnPost = Comment::create(['text' => 'Please soooooon!']); - $commentOnVideo = Comment::create(['text' => 'Balalaika Sings']); - - $postComment = $postCommentor->comments($post)->attach($commentOnPost); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $postComment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $postComment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $postComment->right()); - $this->assertTrue($postComment->exists()); - - $videoComment = $videoCommentor->comments($video)->attach($commentOnVideo); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $videoComment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $videoComment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $videoComment->right()); - $this->assertTrue($videoComment->exists()); - - $this->assertNotEquals($postComment, $videoComment); - } - - public function testAttachingManyModelInstances() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - $commentOnPost = Comment::create(['text' => 'Another Place']); - $anotherCommentOnPost = Comment::create(['text' => 'Here and there']); - $commentOnVideo = Comment::create(['text' => 'When We Meet']); - $anotherCommentOnVideo = Comment::create(['text' => 'That is good']); - - // Comment on post and video - $postComments = $postCommentor->comments($post)->attach([$commentOnPost, $anotherCommentOnPost]); - foreach ($postComments as $comment) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $comment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $comment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $comment->right()); - $this->assertTrue($comment->exists()); - } - - $videoComments = $videoCommentor->comments($video)->attach([$commentOnVideo, $anotherCommentOnVideo]); - foreach ($videoComments as $comment) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $comment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $comment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $comment->right()); - $this->assertTrue($comment->exists()); - } - - $this->assertNotEquals($postComments, $videoComments); - } - - public function testAttachingNonExistingModelIds() - { - $user = User::create(['name' => 'Hmm...']); - $user->posts()->create(['title' => 'A little posty post.']); - $post = $user->posts()->first(); - - $this->expectException(ModelNotFoundException::class); - $user->comments($post)->attach(9999999999); - } - - public function testDetachingModelById() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - // Comment on post and video - $commentOnPost = Comment::create(['text' => 'Please soooooon!']); - $commentOnVideo = Comment::create(['text' => 'Balalaika Sings']); - - $postComment = $postCommentor->comments($post)->attach($commentOnPost); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $postComment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $postComment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $postComment->right()); - $this->assertTrue($postComment->exists()); - - $videoComment = $videoCommentor->comments($video)->attach($commentOnVideo); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\HyperEdge', $videoComment); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $videoComment->left()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Edges\EdgeOut', $videoComment->right()); - $this->assertTrue($videoComment->exists()); - - $this->assertNotEquals($postComment, $videoComment); - - $edges = $postCommentor->comments($post)->edges(); - $this->assertNotEmpty($edges); - - $this->assertTrue($postCommentor->comments($post)->detach($commentOnPost)); - - $edges = $postCommentor->comments($post)->edges(); - $this->assertEmpty($edges); - } - - public function testSyncingModelIds() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - // Comment on post and video - $commentOnPost = Comment::create(['text' => 'Please soooooon!']); - $anotherCommentOnPost = Comment::create(['text' => 'Balalaika Sings']); - - $postComment = $postCommentor->comments($post)->attach($commentOnPost); - - $user->comments($post)->sync([$anotherCommentOnPost->id]); - - $edges = $user->comments($post)->edges(); - - $edgesIds = array_map(function ($edge) { return $edge->getRelated()->getKey(); }, $edges->toArray()); - $this->assertTrue(in_array($anotherCommentOnPost->id, $edgesIds)); - $this->assertFalse(in_array($commentOnPost->id, $edgesIds)); - - foreach ($edges as $edge) { - $edge->delete(); - } - } - - public function testSyncingUpdatesModels() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - // Comment on post and video - $commentOnPost = Comment::create(['text' => 'Please soooooon!']); - $anotherCommentOnPost = Comment::create(['text' => 'Balalaika Sings']); - - $postComment = $postCommentor->comments($post)->attach($commentOnPost); - - $user->comments($post)->sync([$commentOnPost->id, $anotherCommentOnPost->id]); - - $edges = $user->comments($post)->edges(); - - $edgesIds = array_map(function ($edge) { return $edge->getRelated()->getKey(); }, $edges->toArray()); - $this->assertTrue(in_array($anotherCommentOnPost->id, $edgesIds)); - $this->assertTrue(in_array($commentOnPost->id, $edgesIds)); - - foreach ($edges as $edge) { - $edge->delete(); - } - } - - public function testSyncingWithAttributes() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - // Comment on post and video - $commentOnPost = Comment::create(['text' => 'Please soooooon!']); - $anotherCommentOnPost = Comment::create(['text' => 'Balalaika Sings']); - - $postComment = $postCommentor->comments($post)->attach($commentOnPost); - - $user->comments($post)->sync([ - $commentOnPost->id => ['feeling' => 'happy'], - $anotherCommentOnPost->id => ['feeling' => 'sad'], - ]); - - $edges = $user->comments($post)->edges(); - - $edgesIds = array_map(function ($edge) { return $edge->getRelated()->getKey(); }, $edges->toArray()); - $this->assertTrue(in_array($anotherCommentOnPost->id, $edgesIds)); - $this->assertTrue(in_array($commentOnPost->id, $edgesIds)); - - $expectedEdgesTypes = ['sad', 'happy']; - - foreach ($edges as $key => $edge) { - $attributes = $edge->toArray(); - $this->assertArrayHasKey('feeling', $attributes); - $this->assertTrue(in_array($edge->feeling, $expectedEdgesTypes)); - $index = array_search($edge->feeling, $expectedEdgesTypes); - unset($expectedEdgesTypes[$index]); - $edge->delete(); - } - } - - public function testDynamicLoadingMorphedModel() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - $commentOnPost = Comment::create(['text' => 'Please soooooon!']); - $postComment = $postCommentor->comments($post)->attach($commentOnPost); - - $commentOnVideo = new Comment(['title' => 'When We Meet', 'url' => 'http://some.url']); - $videoComment = $videoCommentor->comments($video)->attach($commentOnVideo); - - $post = Post::find($post->id); - foreach ($post->comments as $comment) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Comment', $comment); - $this->assertTrue($comment->exists); - $this->assertGreaterThanOrEqual(0, $comment->id); - $this->assertEquals($commentOnPost->toArray(), $comment->toArray()); - } - - $video = Video::find($video->id); - foreach ($video->comments as $comment) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Comment', $comment); - $this->assertTrue($comment->exists); - $this->assertGreaterThanOrEqual(0, $comment->id); - $this->assertEquals($commentOnVideo->toArray(), $comment->toArray()); - } - } - - public function testEagerLoadingMorphedModel() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - $commentOnPost = Comment::create(['text' => 'Please soooooon!']); - $postComment = $postCommentor->comments($post)->attach($commentOnPost); - - $post = Post::with('comments')->find($post->id); - $postRelations = $post->getRelations(); - $this->assertArrayHasKey('comments', $postRelations); - $this->assertCount(1, $postRelations['comments']); - foreach ($postRelations['comments'] as $comment) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Comment', $comment); - $this->assertTrue($comment->exists); - $this->assertGreaterThanOrEqual(0, $comment->id); - $this->assertEquals($commentOnPost->toArray(), $comment->toArray()); - } - - $commentOnVideo = new Comment(['title' => 'When We Meet', 'url' => 'http://some.url']); - $videoComment = $videoCommentor->comments($video)->attach($commentOnVideo); - - $video = Video::with('comments')->find($video->id); - $videoRelations = $video->getRelations(); - $this->assertArrayHasKey('comments', $videoRelations); - $this->assertCount(1, $videoRelations['comments']); - foreach ($videoRelations['comments'] as $comment) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Comment', $comment); - $this->assertTrue($comment->exists); - $this->assertGreaterThanOrEqual(0, $comment->id); - $this->assertEquals($commentOnVideo->toArray(), $comment->toArray()); - } - } - - public function testDynamicLoadingMorphingModels() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - $commentOnPost = Comment::create(['text' => 'Please soooooon!']); - $postComment = $postCommentor->comments($post)->attach($commentOnPost); - - $commentOnVideo = new Comment(['title' => 'When We Meet', 'url' => 'http://some.url']); - $videoComment = $videoCommentor->comments($video)->attach($commentOnVideo); - - $comments = $postCommentor->comments; - $this->assertEquals($commentOnPost->toArray(), $comments->first()->toArray()); - - $comments = $videoCommentor->comments; - $this->assertEquals($commentOnVideo->toArray(), $comments->first()->toArray()); - } - - public function testEagerLoadingMorphingModels() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - // Attach and assert the comment on Post - $commentOnPost = Comment::create(['text' => 'Please soooooon!']); - $postComment = $postCommentor->comments($post)->attach($commentOnPost); - - $userMorph = User::with('comments')->find($postCommentor->id); - $userRelations = $userMorph->getRelations(); - $this->assertArrayHasKey('comments', $userRelations); - $this->assertCount(1, $userRelations['comments']); - $this->assertEquals($commentOnPost->toArray(), $userRelations['comments']->first()->toArray()); - - // Attach and assert the comment on Video - $commentOnVideo = new Comment(['title' => 'When We Meet', 'url' => 'http://some.url']); - $videoComment = $videoCommentor->comments($video)->attach($commentOnVideo); - - $vUserMorph = User::with('comments')->find($videoCommentor->id); - $vUserRelations = $vUserMorph->getRelations(); - $this->assertArrayHasKey('comments', $vUserRelations); - $this->assertCount(1, $userRelations['comments']); - $this->assertEquals($commentOnVideo->toArray(), $vUserRelations['comments']->first()->toArray()); - } - - public function testDynamicLoadingMorphedByModel() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - $commentOnPost = Comment::create(['text' => 'Please soooooon!']); - $postComment = $postCommentor->comments($post)->attach($commentOnPost); - - $postMorph = $commentOnPost->post; - $this->assertTrue($postMorph->exists); - $this->assertGreaterThanOrEqual(0, $postMorph->id); - $this->assertEquals($post->toArray(), $postMorph->toArray()); - - $commentOnVideo = new Comment(['title' => 'When We Meet', 'url' => 'http://some.url']); - $videoComment = $videoCommentor->comments($video)->attach($commentOnVideo); - - $videoMorph = $commentOnVideo->video; - $this->assertTrue($videoMorph->exists); - $this->assertGreaterThanOrEqual(0, $videoMorph->id); - $this->assertEquals($video->toArray(), $videoMorph->toArray()); - } - - public function testEagerLoadingMorphedByModel() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - // Check the post of this comment - $commentOnPost = Comment::create(['text' => 'Please soooooon!']); - $postComment = $postCommentor->comments($post)->attach($commentOnPost); - - $morphedComment = Comment::with('post')->find($commentOnPost->id); - $morphedCommentRelations = $morphedComment->getRelations(); - $this->assertArrayHasKey('post', $morphedCommentRelations); - $this->assertEquals($post->toArray(), $morphedCommentRelations['post']->toArray()); - - // Check the video of this comment - $commentOnVideo = new Comment(['title' => 'When We Meet', 'url' => 'http://some.url']); - $videoComment = $videoCommentor->comments($video)->attach($commentOnVideo); - - $vMorphedComment = Comment::with('video')->find($commentOnVideo->id); - $vMorphedCommentRelations = $vMorphedComment->getRelations(); - $this->assertArrayHasKey('video', $vMorphedCommentRelations); - $this->assertEquals($video->toArray(), $vMorphedCommentRelations['video']->toArray()); - } - - public function testDynamicLoadingMorphToModel() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - // Check the post of this comment - $commentOnPost = Comment::create(['text' => 'Please soooooon!']); - $postComment = $postCommentor->comments($post)->attach($commentOnPost); - - $commentablePost = $commentOnPost->commentable; - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Post', $commentablePost); - $this->assertEquals($post->toArray(), $commentablePost->toArray()); - - // Check the video of this comment - $commentOnVideo = new Comment(['title' => 'When We Meet', 'url' => 'http://some.url']); - $videoComment = $videoCommentor->comments($video)->attach($commentOnVideo); - - $commentableVideo = $commentOnVideo->commentable; - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Video', $commentableVideo); - $this->assertEquals($video->toArray(), $commentableVideo->toArray()); - } - - public function testEagerLoadingMorphToModel() - { - $user = User::create(['name' => 'Hmm...']); - $postCommentor = User::create(['name' => 'I Comment On Posts']); - $videoCommentor = User::create(['name' => 'I Comment On Videos']); - // create the user's post and video - $user->posts()->create(['title' => 'Another Place', 'body' => 'To Go..']); - $user->videos()->create(['title' => 'When We Meet', 'url' => 'http://some.url']); - // Grab them back - $post = $user->posts->first(); - $video = $user->videos->first(); - - // Check the post of this comment - $commentOnPost = Comment::create(['text' => 'Please soooooon!']); - $postComment = $postCommentor->comments($post)->attach($commentOnPost); - - $morphedPostComment = Comment::with('commentable')->find($commentOnPost->id); - $morphedCommentRelations = $morphedPostComment->getRelations(); - - $this->assertArrayHasKey('commentable', $morphedCommentRelations); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Post', $morphedCommentRelations['commentable']); - $this->assertEquals($post->toArray(), $morphedCommentRelations['commentable']->toArray()); - - // // Check the video of this comment - $commentOnVideo = new Comment(['title' => 'When We Meet', 'url' => 'http://some.url']); - $videoComment = $videoCommentor->comments($video)->attach($commentOnVideo); - - $morphedVideoComment = Comment::with('commentable')->find($commentOnVideo->id); - $morphedVideoCommentRelations = $morphedVideoComment->getRelations(); - - $this->assertArrayHasKey('commentable', $morphedVideoCommentRelations); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Video', $morphedVideoCommentRelations['commentable']); - $this->assertEquals($video->toArray(), $morphedVideoCommentRelations['commentable']->toArray()); - } -} - -class User extends Model -{ - protected $label = 'User'; - - protected $fillable = ['name']; - - public function comments($model = null) - { - return $this->hyperMorph($model, 'Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Comment', 'COMMENTED', 'ON'); - } - - public function posts() - { - return $this->hasMany('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Post', 'POSTED'); - } - - public function videos() - { - return $this->hasMany('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Video', 'UPLOADED'); - } -} - -class Post extends Model -{ - protected $label = 'Post'; - - protected $fillable = ['title', 'body']; - - public function comments() - { - return $this->morphMany('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Comment', 'ON'); - } -} - -class Video extends Model -{ - protected $label = 'Video'; - - protected $fillable = ['title', 'url']; - - public function comments() - { - return $this->morphMany('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Comment', 'ON'); - } -} - -class Comment extends Model -{ - protected $label = 'Comment'; - - protected $fillable = ['text']; - - public function commentable() - { - return $this->morphTo(); - } - - public function post() - { - return $this->morphTo('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Post', 'ON'); - } - - public function video() - { - return $this->morphTo('Vinelab\NeoEloquent\Tests\Functional\Relations\HyperMorphTo\Video', 'ON'); - } -} diff --git a/tests/functional/QueryScopesTest.php b/tests/functional/QueryScopesTest.php deleted file mode 100644 index ae1a6c56..00000000 --- a/tests/functional/QueryScopesTest.php +++ /dev/null @@ -1,65 +0,0 @@ -where('alias', 'tesla'); - } - - public function scopeStupidDickhead($query) - { - return $query->where('alias', 'edison'); - } -} - -class QueryScopesTest extends TestCase -{ - public function tearDown(): void - { - M::close(); - - $all = Misfit::all(); - $all->each(function ($u) { $u->delete(); }); - - parent::tearDown(); - } - - public function setUp(): void - { - parent::setUp(); - - $resolver = M::mock('Illuminate\Database\ConnectionResolverInterface'); - $resolver->shouldReceive('connection')->andReturn($this->getConnectionWithConfig('default')); - Misfit::setConnectionResolver($resolver); - - $this->t = Misfit::create([ - 'name' => 'Nikola Tesla', - 'alias' => 'tesla', - ]); - - $this->e = misfit::create([ - 'name' => 'Thomas Edison', - 'alias' => 'edison', - ]); - } - - public function testQueryScopes() - { - $t = Misfit::kingOfScience()->first(); - $this->assertEquals($this->t->toArray(), $t->toArray()); - - $e = Misfit::stupidDickhead()->first(); - $this->assertEquals($this->e->toArray(), $e->toArray()); - } -} diff --git a/tests/functional/QueryingRelationsTest.php b/tests/functional/QueryingRelationsTest.php deleted file mode 100644 index 853d032c..00000000 --- a/tests/functional/QueryingRelationsTest.php +++ /dev/null @@ -1,966 +0,0 @@ - 'I have no comments =(', 'body' => 'None!']); - $postWithComment = Post::create(['title' => 'Nananana', 'body' => 'Commentmaaan']); - $postWithTwoComments = Post::create(['title' => 'I got two']); - $postWithTenComments = Post::create(['tite' => 'Up yours posts, got 10 here']); - - $comment = new Comment(['text' => 'food']); - $postWithComment->comments()->save($comment); - - // add two comments to $postWithTwoComments - for ($i = 0; $i < 2; ++$i) { - $postWithTwoComments->comments()->create(['text' => "Comment $i"]); - } - // add ten comments to $postWithTenComments - for ($i = 0; $i < 10; ++$i) { - $postWithTenComments->comments()->create(['text' => "Comment $i"]); - } - - $allPosts = Post::get(); - $this->assertEquals(4, count($allPosts)); - - $posts = Post::has('comments')->get(); - $this->assertEquals(3, count($posts)); - $expectedHasComments = [$postWithComment->id, $postWithTwoComments->id, $postWithTenComments->id]; - foreach ($posts as $key => $post) { - $this->assertTrue(in_array($post->id, $expectedHasComments)); - } - - $postsWithMoreThanOneComment = Post::has('comments', '>=', 2)->get(); - $this->assertEquals(2, count($postsWithMoreThanOneComment)); - $expectedWithMoreThanOne = [$postWithTwoComments->id, $postWithTenComments->id]; - foreach ($postsWithMoreThanOneComment as $post) { - $this->assertTrue(in_array($post->id, $expectedWithMoreThanOne)); - } - - $postWithTen = Post::has('comments', '=', 10)->get(); - $this->assertEquals(1, count($postWithTen)); - $this->assertEquals($postWithTenComments->toArray(), $postWithTen->first()->toArray()); - } - - public function testQueryingNestedHas() - { - // user with a role that has only one permission - $user = User::create(['name' => 'cappuccino']); - $role = Role::create(['alias' => 'pikachu']); - $permission = Permission::create(['title' => 'Elephant', 'alias' => 'elephant']); - $role->permissions()->save($permission); - $user->roles()->save($role); - - // user with a role that has 2 permissions - $userWithTwo = User::create(['name' => 'frappe']); - $roleWithTwo = Role::create(['alias' => 'pikachu']); - $permissionOne = Permission::create(['title' => 'Goomba', 'alias' => 'goomba']); - $permissionTwo = Permission::create(['title' => 'Boomba', 'alias' => 'boomba']); - $roleWithTwo->permissions()->saveMany([$permissionOne, $permissionTwo]); - $userWithTwo->roles()->save($roleWithTwo); - - - // user with a role that has no permission - $user2 = User::Create(['name' => 'u2']); - $role2 = Role::create(['alias' => 'nosperm']); - - $user2->roles()->save($role2); - - // get the users where their roles have at least one permission. - $found = User::has('roles.permissions')->get(); - - $this->assertEquals(2, count($found)); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', $found[1]); - $this->assertEquals($userWithTwo->toArray(), $found->where('name', 'frappe')->first()->toArray()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', $found[0]); - $this->assertEquals($user->toArray(), $found->where('name', 'cappuccino')->first()->toArray()); - - $moreThanOnePermission = User::has('roles.permissions', '>=', 2)->get(); - $this->assertEquals(1, count($moreThanOnePermission)); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', $moreThanOnePermission[0]); - $this->assertEquals($userWithTwo->toArray(), $moreThanOnePermission[0]->toArray()); - } - - public function testQueryingWhereHasOne() - { - $mrAdmin = User::create(['name' => 'Rundala']); - $anotherAdmin = User::create(['name' => 'Makhoul']); - $mrsEditor = User::create(['name' => 'Mr. Moonlight']); - $mrsManager = User::create(['name' => 'Batista']); - $anotherManager = User::create(['name' => 'Quin Tukee']); - - $admin = Role::create(['alias' => 'admin']); - $editor = Role::create(['alias' => 'editor']); - $manager = Role::create(['alias' => 'manager']); - - $mrAdmin->roles()->save($admin); - $anotherAdmin->roles()->save($admin); - $mrsEditor->roles()->save($editor); - $mrsManager->roles()->save($manager); - $anotherManager->roles()->save($manager); - - // check admins - $admins = User::whereHas('roles', function ($q) { $q->where('alias', 'admin'); })->get(); - $this->assertEquals(2, count($admins)); - $expectedAdmins = [$mrAdmin, $anotherAdmin]; - $expectedAdmins = array_map(function ($admin) { - return $admin->toArray(); - }, $expectedAdmins); - foreach ($admins as $key => $admin) { - $this->assertContains($admin->toArray()['id'], array_map(static fn(array $admin) => $admin['id'], $expectedAdmins)); - } - // check editors - $editors = User::whereHas('roles', function ($q) { $q->where('alias', 'editor'); })->get(); - $this->assertEquals(1, count($editors)); - $this->assertEquals($mrsEditor->toArray(), $editors->first()->toArray()); - // check managers - $expectedManagers = [$mrsManager, $anotherManager]; - $managers = User::whereHas('roles', function ($q) { $q->where('alias', 'manager'); })->get(); - $this->assertEquals(2, count($managers)); - $expectedManagers = array_map(function ($manager) { - return $manager->toArray(); - }, $expectedManagers); - foreach ($managers as $key => $manager) { - $this->assertContains($manager->toArray()['id'], array_map(static fn(array $manager) => $manager['id'], $expectedManagers)); - } - } - - public function testQueryingWhereHasById() - { - $user = User::create(['name' => 'cappuccino']); - $role = Role::create(['alias' => 'pikachu']); - - $user->roles()->save($role); - - $found = User::whereHas('roles', function ($q) use ($role) { - $q->where('id', $role->getKey()); - })->first(); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', $found); - $this->assertEquals($user->toArray(), $found->toArray()); - } - - public function testQueryingParentWithWhereHas() - { - $user = User::create(['name' => 'cappuccino']); - $role = Role::create(['alias' => 'pikachu']); - - $user->roles()->save($role); - - $found = User::whereHas('roles', function ($q) use ($role) { - $q->where('id', $role->id); - })->where('id', $user->id)->first(); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', $found); - $this->assertEquals($user->toArray(), $found->toArray()); - } - - public function testQueryingParentWithMultipleWhereHas() - { - $user = User::create(['name' => 'cappuccino']); - $role = Role::create(['alias' => 'pikachu']); - $account = Account::create(['guid' => uniqid()]); - - $user->roles()->save($role); - $user->account()->save($account); - - $found = User::whereHas('roles', function ($q) use ($role) { $q->where('id', $role->id); }) - ->whereHas('account', function ($q) use ($account) { $q->where('id', $account->id); }) - ->where('id', $user->id)->first(); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', $found); - $this->assertEquals($user->toArray(), $found->toArray()); - } - - public function testQueryingNestedWhereHasUsingId() - { - // user with a role that has only one permission - $user = User::create(['name' => 'cappuccino']); - $role = Role::create(['alias' => 'pikachu']); - $permission = Permission::create(['title' => 'Elephant', 'alias' => 'elephant']); - $role->permissions()->save($permission); - $user->roles()->save($role); - - // user with a role that has 2 permissions - $userWithTwo = User::create(['name' => 'cappuccino']); - $roleWithTwo = Role::create(['alias' => 'pikachu']); - $permissionOne = Permission::create(['title' => 'Goomba', 'alias' => 'goomba']); - $permissionTwo = Permission::create(['title' => 'Boomba', 'alias' => 'boomba']); - $roleWithTwo->permissions()->saveMany([$permissionOne, $permissionTwo]); - $userWithTwo->roles()->save($roleWithTwo); - - $found = User::whereHas('roles', function($q) use($role, $permission) { - $q->where($role->getKeyName(), $role->getKey()); - $q->whereHas('permissions', function($q) use($permission) { - $q->where($permission->getKeyName(), $permission->getKey()); - }); - })->get(); - - $this->assertEquals(1, count($found)); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', $found->first()); - $this->assertEquals($user->toArray(), $found->first()->toArray()); - } - - public function testQueryingNestedWhereHasUsingProperty() - { - // user with a role that has only one permission - $user = User::create(['name' => 'cappuccino']); - $role = Role::create(['alias' => 'pikachu']); - $permission = Permission::create(['title' => 'Elephant', 'alias' => 'elephant']); - $role->permissions()->save($permission); - $user->roles()->save($role); - - // user with a role that has 2 permissions - $userWithTwo = User::create(['name' => 'cappuccino']); - $roleWithTwo = Role::create(['alias' => 'pikachu']); - $permissionOne = Permission::create(['title' => 'Goomba', 'alias' => 'goomba']); - $permissionTwo = Permission::create(['title' => 'Boomba', 'alias' => 'boomba']); - $roleWithTwo->permissions()->saveMany([$permissionOne, $permissionTwo]); - $userWithTwo->roles()->save($roleWithTwo); - - $found = User::whereHas('roles', function($q) use($role, $permission) { - $q->where('alias', $role->alias); - $q->whereHas('permissions', function($q) use($permission) { - $q->where('alias', $permission->alias); - }); - })->get(); - - $this->assertEquals(1, count($found)); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', $found->first()); - $this->assertEquals($user->toArray(), $found->first()->toArray()); - } - - public function testCreatingModelWithSingleRelation() - { - $account = ['guid' => uniqid()]; - $user = User::createWith(['name' => 'Misteek'], compact('account')); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', $user); - $this->assertTrue($user->exists); - $this->assertGreaterThanOrEqual(0, $user->id); - - $related = $user->account; - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Account', $related); - $this->assertNotNull($related->created_at); - $this->assertNotNull($related->updated_at); - - $attrs = $related->toArray(); - unset($attrs['id']); - unset($attrs['created_at']); - unset($attrs['updated_at']); - $this->assertEquals($account, $attrs); - } - - public function testCreatingModelWithRelations() - { - // Creating a role with its permissions. - $role = ['title' => 'Admin', 'alias' => 'admin']; - - $permissions = [ - new Permission(['title' => 'Create Records', 'alias' => 'create', 'dodid' => 'done']), - new Permission(['title' => 'Read Records', 'alias' => 'read', 'dont be so' => 'down']), - ['title' => 'Update Records', 'alias' => 'update'], - ['title' => 'Delete Records', 'alias' => 'delete'], - ]; - - $role = Role::createWith($role, compact('permissions')); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Role', $role); - $this->assertTrue($role->exists); - $this->assertGreaterThanOrEqual(0, $role->id); - - foreach ($role->permissions as $key => $permission) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Permission', $permission); - $this->assertGreaterThan(0, $permission->id); - $this->assertNotNull($permission->created_at); - $this->assertNotNull($permission->updated_at); - $attrs = $permission->toArray(); - unset($attrs['id']); - unset($attrs['created_at']); - unset($attrs['updated_at']); - if ($permissions[$key] instanceof Permission) { - $permission = $permissions[$key]; - $permission = $permission->toArray(); - unset($permission['id']); - unset($permission['created_at']); - unset($permission['updated_at']); - $this->assertEquals($permission, $attrs); - } else { - $this->assertEquals($permissions[$key], $attrs); - } - } - } - - public function testCreatingModelWithMultipleRelationTypes() - { - $post = ['title' => 'Trip to Bedlam', 'body' => 'It was wonderful! Check the embedded media']; - - $photos = [ - [ - 'url' => 'http://somewere.in.bedlam.net', - 'caption' => 'Gunatanamo', - 'metadata' => '...', - ], - [ - 'url' => 'http://another-place.in.bedlam.net', - 'caption' => 'Gunatanamo', - 'metadata' => '...', - ], - ]; - - $videos = [ - [ - 'title' => 'Fun at the borders', - 'description' => 'Once upon a time...', - 'stream_url' => 'http://stream.that.shit.io', - 'thumbnail' => 'http://sneak.peek.io', - ], - ]; - - $post = Post::createWith($post, compact('photos', 'videos')); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Post', $post); - $this->assertTrue($post->exists); - $this->assertGreaterThanOrEqual(0, $post->id); - - foreach ($post->photos as $key => $photo) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Photo', $photo); - $this->assertGreaterThan(0, $photo->id); - $this->assertNotNull($photo->created_at); - $this->assertNotNull($photo->updated_at); - $attrs = $photo->toArray(); - unset($attrs['id']); - unset($attrs['created_at']); - unset($attrs['updated_at']); - $this->assertEquals($photos[$key], $attrs); - } - - $video = $post->videos->first(); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Video', $video); - $this->assertNotNull($video->created_at); - $this->assertNotNull($video->updated_at); - $attrs = $video->toArray(); - unset($attrs['id']); - unset($attrs['created_at']); - unset($attrs['updated_at']); - $this->assertEquals($videos[0], $attrs); - } - - public function testCreatingModelWithSingleInverseRelation() - { - $user = ['name' => 'Some Name']; - $account = Account::createWith(['guid' => 'globalid'], compact('user')); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Account', $account); - $this->assertTrue($account->exists); - $this->assertGreaterThanOrEqual(0, $account->id); - - $related = $account->user; - $this->assertNotNull($related->created_at); - $this->assertNotNull($related->updated_at); - $attrs = $related->toArray(); - unset($attrs['id']); - unset($attrs['created_at']); - unset($attrs['updated_at']); - $this->assertEquals($attrs, $user); - } - - public function testCreatingModelWithMultiInverseRelations() - { - $users = new User(['name' => 'safastak']); - $role = Role::createWith(['alias' => 'admin'], compact('users')); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Role', $role); - $this->assertTrue($role->exists); - $this->assertGreaterThanOrEqual(0, $role->id); - - $related = $role->users->first(); - $this->assertNotNull($related->created_at); - $this->assertNotNull($related->updated_at); - $attrs = $related->toArray(); - unset($attrs['id']); - unset($attrs['created_at']); - unset($attrs['updated_at']); - $usersArray = $users->toArray(); - unset($usersArray['id']); - unset($usersArray['created_at']); - unset($usersArray['updated_at']); - $this->assertEquals($attrs, $usersArray); - } - - public function testCreatingModelWithAttachedRelatedModels() - { - $tag1 = Tag::create(['title' => 'php']); - $tag2 = Tag::create(['title' => 'development']); - - $tags = [$tag1, $tag2]; - $post = Post::createWith(['title' => '...', 'body' => '...'], compact('tags')); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Post', $post); - - $related = $post->tags; - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Collection', $related); - $this->assertEquals(2, count($related)); - - foreach ($related as $key => $tag) { - $this->assertEquals($tags[$key]->toArray(), $tag->toArray()); - } - } - - /** - * Regression test for issue where createWith ignores creating timestamps for record. - * - * @see https://github.com/Vinelab/NeoEloquent/issues/17 - */ - public function testCreateWithAddsTimestamps() - { - $tag1 = Tag::create(['title' => 'php']); - $tag2 = Tag::create(['title' => 'development']); - $tags = [$tag1->getKey(), $tag2->getKey()]; - - $post = Post::createWith(['title' => '...', 'body' => '...'], compact('tags')); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Post', $post); - - $this->assertNotNull($post->created_at); - $this->assertNotNull($post->updated_at); - - $related = $post->tags; - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Collection', $related); - $this->assertEquals(2, count($related)); - - foreach ($related as $key => $tag) { - $expected = 'tag'.($key + 1); - $this->assertEquals($$expected->toArray(), $tag->toArray()); - } - } - - public function testCreatWithPassesThroughFillables() - { - $tag1 = Tag::create(['title' => 'php']); - $tag2 = Tag::create(['title' => 'development']); - $tags = [$tag1->getKey(), $tag2->getKey()]; - - $post = Post::createWith(['title' => '...', 'body' => '...', 'mother' => 'something', 'father' => 'wanted'], compact('tags')); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Post', $post); - - $this->assertNull($post->mother); - $this->assertNull($post->father); - $this->assertNotNull($post->created_at); - $this->assertNotNull($post->updated_at); - - $related = $post->tags; - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Collection', $related); - $this->assertEquals(2, count($related)); - - foreach ($related as $key => $tag) { - $expected = 'tag'.($key + 1); - $this->assertEquals($$expected->toArray(), $tag->toArray()); - } - } - - public function testCreatingModelWithNullAndBooleanValues() - { - $tag1 = Tag::create(['title' => 'php']); - $tag2 = Tag::create(['title' => 'development']); - $tags = [$tag1->getKey(), $tag2->getKey()]; - - $post = Post::createWith(['title' => false, 'body' => true, 'summary' => null], compact('tags')); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Post', $post); - - $this->assertFalse($post->title); - $this->assertTrue($post->body); - $this->assertNull($post->summary); - $this->assertNotNull($post->created_at); - $this->assertNotNull($post->updated_at); - - $related = $post->tags; - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Collection', $related); - $this->assertEquals(2, count($related)); - - foreach ($related as $key => $tag) { - $expected = 'tag'.($key + 1); - $this->assertEquals($$expected->toArray(), $tag->toArray()); - } - } - - public function testCreatingModeWithAttachedModelIds() - { - $tag1 = Tag::create(['title' => 'php']); - $tag2 = Tag::create(['title' => 'development']); - - $tags = [$tag1->getKey(), $tag2->getKey()]; - $post = Post::createWith(['title' => '...', 'body' => '...'], compact('tags')); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Post', $post); - - $related = $post->tags; - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Collection', $related); - $this->assertEquals(2, count($related)); - - foreach ($related as $key => $tag) { - $expected = 'tag'.($key + 1); - $this->assertEquals($$expected->toArray(), $tag->toArray()); - } - } - - public function testCreatingModelWithAttachedSingleId() - { - $tag = Tag::create(['title' => 'php']); - $post = Post::createWith(['title' => '...', 'body' => '...'], ['tags' => $tag->getKey()]); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Post', $post); - - $related = $post->tags; - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Collection', $related); - $this->assertEquals(1, count($related)); - $this->assertEquals($tag->toArray(), $related->first()->toArray()); - } - - public function testCreatingModelWithAttachedSingleModel() - { - $tag = Tag::create(['title' => 'php']); - $post = Post::createWith(['title' => '...', 'body' => '...'], ['tags' => $tag]); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Post', $post); - - $related = $post->tags; - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Collection', $related); - $this->assertEquals(1, count($related)); - $this->assertEquals($tag->toArray(), $related->first()->toArray()); - } - - public function testCreatingModelWithMixedRelationsAndPassingCollection() - { - $tag = Tag::create(['title' => 'php']); - $tags = [ - $tag, - ['title' => 'developer'], - new Tag(['title' => 'laravel']), - ]; - - $post = Post::createWith(['title' => 'foo', 'body' => 'bar'], compact('tags')); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Post', $post); - $related = $post->tags; - $this->assertInstanceOf('Vinelab\NeoEloquent\Eloquent\Collection', $related); - $this->assertEquals(3, count($related)); - - $tags = Tag::all(); - - $another = Post::createWith(['title' => 'foo', 'body' => 'bar'], compact('tags')); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Post', $another); - $this->assertEquals(3, count($related)); - } - - /** - * Regression for issue #9. - * - * @see https://github.com/Vinelab/NeoEloquent/issues/9 - */ - public function testCreateModelWithMultiRelationOfSameRelatedModel() - { - $post = Post::createWith(['title' => 'tayta', 'body' => 'one hot bowy'], [ - 'photos' => ['url' => 'my.photo.url'], - 'cover' => ['url' => 'my.cover.url'], - ]); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Post', $post); - - $this->assertEquals('my.photo.url', $post->photos->first()->url); - $this->assertEquals('my.cover.url', $post->cover->url); - } - - /** - * Regression test for creating recursively connected models. - * - * @see https://github.com/Vinelab/NeoEloquent/issues/7 - */ - public function testCreatingModelWithExistingRecursivelyRelatedModel() - { - $jon = User::create(['name' => 'Jon Ronson']); - $morgan = User::create(['name' => 'Morgan Spurlock']); - - $user = User::createWith(['name' => 'Ken Robinson'], [ - 'colleagues' => [$morgan, $jon], - ]); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', $user); - } - - public function testEagerLoadingNestedRelationship() - { - $user = User::create(['name' => 'cappuccino']); - $role = Role::createWith(['alias' => 'pikachu'], ['permissions' => ['title' => 'Perr', 'alias' => 'perr']]); - - $user->roles()->save($role); - // Eager load so that when we assert we make sure they're there - $user->roles->first()->permissions; - - $found = User::with('roles.permissions') - ->whereHas('roles', function ($q) use ($role) { $q->where('id', $role->id); }) - ->first(); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', $found); - $this->assertArrayHasKey('roles', $found->getRelations()); - $this->assertArrayHasKey('permissions', $found->roles->first()->getRelations()); - $this->assertEquals($user->toArray(), $found->toArray()); - } - - public function testInverseEagerLoadingOneNestedRelationship() - { - $user = User::createWith(['name' => 'cappuccino'], ['account' => ['guid' => 'anID']]); - $role = Role::create(['alias' => 'pikachu']); - - $user->roles()->save($role); - // Eager load so that when we assert we make sure they're there - $acc = $role->users->first()->account; - - $roleFound = Role::with('users.account') - ->whereHas('users', function ($q) use ($user) { $q->where('id', $user->getKey()); }) - ->first(); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Role', $roleFound); - $this->assertArrayHasKey('users', $roleFound->getRelations()); - $this->assertArrayHasKey('account', $roleFound->users->first()->getRelations()); - $this->assertEquals('anID', $roleFound->users->first()->account->guid); - $this->assertEquals($role->toArray(), $roleFound->toArray()); - } - - public function testDoubleInverseEagerLoadingBelongsToRelationship() - { - $user = User::createWith(['name' => 'cappuccino'], ['organization' => ['name' => 'Pokemon']]); - // Eager load so that when we assert we make sure they're there - $role = Role::create(['alias' => 'pikachu']); - - $user->roles()->save($role); - // Eager load so that when we assert we make sure they're there - $org = $role->users->first()->organization; - - $roleFound = Role::with('users.organization') - ->whereHas('users', function ($q) use ($user) { $q->where('id', $user->getKey()); }) - ->first(); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Role', $roleFound); - $this->assertArrayHasKey('users', $roleFound->getRelations()); - $this->assertArrayHasKey('organization', $roleFound->users->first()->getRelations()); - $this->assertEquals('Pokemon', $roleFound->users->first()->organization->name); - $this->assertEquals($role->toArray(), $roleFound->toArray()); - } - - public function testQueryingRelatedModel() - { - $user = User::createWith(['name' => 'Beluga'], [ - 'roles' => [ - ['title' => 'Read Things', 'alias' => 'read'], - ['title' => 'Write Things', 'alias' => 'write'], - ], - ]); - - $read = Role::where('alias', 'read')->first(); - $this->assertEquals('read', $read->alias); - $readFound = $user->roles()->where('alias', 'read')->first(); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Role', $readFound); - $this->assertEquals($read, $readFound); - - $write = Role::where('alias', 'write')->first(); - $this->assertEquals('write', $write->alias); - $writeFound = $user->roles()->where('alias', 'write')->first(); - $this->assertEquals($write, $writeFound); - } - - public function testDirectRecursiveRelationQuery() - { - $user = User::createWith(['name' => 'captain'], ['colleagues' => ['name' => 'acme']]); - $acme = User::where('name', 'acme')->first(); - $found = $user->colleagues()->where('name', 'acme')->first(); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', $found); - - $this->assertEquals($acme, $found); - } - - public function testSavingCreateWithRelationWithDateTimeAndCarbonInstances() - { - $yesterday = Carbon::now()->subDay(); - $dt = new DateTime(); - - $user = User::createWith(['name' => 'Some Name', 'dob' => $yesterday], - ['colleagues' => ['name' => 'Protectron', 'dob' => $dt], - ]); - - $houwe = User::first(); - $colleague = $houwe->colleagues()->first(); - - $this->assertEquals($yesterday->format(User::getDateFormat()), $houwe->dob); - $this->assertEquals($dt->format(User::getDateFormat()), $colleague->dob); - } - - public function testSavingRelationWithDateTimeAndCarbonInstances() - { - $user = User::create(['name' => 'Andrew Hale']); - $yesterday = Carbon::now(); - $brother = new User(['name' => 'Simon Hale', 'dob' => $yesterday]); - - $dt = new DateTime(); - $someone = User::create(['name' => 'Producer', 'dob' => $dt]); - - $user->colleagues()->save($someone); - $user->colleagues()->save($brother); - - $andrew = User::first(); - - $colleagues = $andrew->colleagues()->get(); - $this->assertEquals($dt->format(User::getDateFormat()), $colleagues[0]->dob); - $this->assertEquals($yesterday->format(User::getDateFormat()), $colleagues[1]->dob); - } - - public function testCreateWithReturnsRelatedModelsAsRelations() - { - $user = Post::createWith( - ['title' => 'foo tit', 'body' => 'some body'], - [ - 'cover' => ['url' => 'http://url'], - 'tags' => ['title' => 'theTag'], - ] - ); - - $relations = $user->getRelations(); - - $this->assertArrayHasKey('cover', $relations); - $cover = $user->toArray()['cover']; - $this->assertArrayHasKey('id', $cover); - $this->assertEquals('http://url', $cover['url']); - - $this->assertArrayHasKey('tags', $relations); - $tags = $user->toArray()['tags']; - $this->assertCount(1, $tags); - - $this->assertNotEmpty($tags[0]['id']); - $this->assertEquals('theTag', $tags[0]['title']); - } - - public function testEagerloadingRelationships() - { - $fooPost = Post::createWith( - ['title' => 'foo tit', 'body' => 'some body'], - [ - 'cover' => ['url' => 'http://url'], - 'tags' => ['title' => 'theTag'], - ] - ); - - $anotherPost = Post::createWith( - ['title' => 'another tit', 'body' => 'another body'], - [ - 'cover' => ['url' => 'http://another.url'], - 'tags' => ['title' => 'anotherTag'], - ] - ); - - $posts = Post::with(['cover', 'tags'])->get(); - - $this->assertEquals(2, count($posts)); - - foreach ($posts as $post) { - $this->assertNotNull($post->cover); - $this->assertEquals(1, count($post->tags)); - } - - $this->assertEquals('http://url', $posts[0]->cover->url); - $this->assertEquals('theTag', $posts[0]->tags->first()->title); - - $this->assertEquals('http://another.url', $posts[1]->cover->url); - $this->assertEquals('anotherTag', $posts[1]->tags->first()->title); - } - - public function testBulkDeletingOutgoingRelation() - { - $fooPost = Post::createWith( - ['title' => 'foo tit', 'body' => 'some body'], - [ - 'cover' => ['url' => 'http://url'], - 'tags' => [ - ['title' => 'theTag'], - ['title' => 'anotherTag'], - ], - ] - ); - - $fooPost->tags()->delete(); - - $this->assertEquals(0, count(Post::first()->tags)); - } - - public function testBulkDeletingIncomingRelation() - { - $users = [new User(['name' => 'safastak']), new User(['name' => 'boukharest'])]; - $role = Role::createWith(['alias' => 'admin'], compact('users')); - - $role->users()->delete(); - - $this->assertEquals(0, count(Role::first()->users)); - } -} - -class User extends Model -{ - protected $label = 'User'; - - protected $fillable = ['name', 'dob']; - - public function roles() - { - return $this->hasMany('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Role', 'PERMITTED'); - } - - public function account() - { - return $this->hasOne('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Account', 'ACCOUNT'); - } - - public function colleagues() - { - return $this->hasMany('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', 'COLLEAGUE_OF'); - } - - public function organization() - { - return $this->belongsTo('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Organization', 'MEMBER_OF'); - } -} - -class Account extends Model -{ - protected $label = 'Account'; - - protected $fillable = ['guid']; - - public function user() - { - return $this->belongsTo('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', 'ACCOUNT'); - } -} - -class Organization extends Model -{ - protected $label = 'Organization'; - - protected $fillable = ['name']; - - public function members() - { - return $this->hasMany('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', 'MEMBER_OF'); - } -} - -class Role extends Model -{ - protected $label = 'Role'; - - protected $fillable = ['title', 'alias']; - - public function users() - { - return $this->belongsToMany('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\User', 'PERMITTED'); - } - - public function permissions() - { - return $this->hasMany('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Permission', 'ALLOWS'); - } -} - -class Permission extends Model -{ - protected $label = 'Permission'; - - protected $fillable = ['title', 'alias']; - - public function roles() - { - return $this->belongsToMany('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Role', 'ALLOWS'); - } -} - -class Post extends Model -{ - protected $label = 'Post'; - - protected $fillable = ['title', 'body', 'summary']; - - public function photos() - { - return $this->hasMany('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Photo', 'PHOTO'); - } - - public function cover() - { - return $this->hasOne('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Photo', 'COVER'); - } - - public function videos() - { - return $this->hasMany('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Video', 'VIDEO'); - } - - public function comments() - { - return $this->hasMany('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Comment', 'COMMENT'); - } - - public function tags() - { - return $this->hasMany('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Tag', 'TAG'); - } -} - -class Tag extends Model -{ - protected $label = 'Tag'; - - protected $fillable = ['title']; -} - -class Photo extends Model -{ - protected $label = 'Photo'; - - protected $fillable = ['url', 'caption', 'metadata']; -} - -class Video extends Model -{ - protected $label = 'Video'; - - protected $fillable = ['title', 'description', 'stream_url', 'thumbnail']; -} - -class Comment extends Model -{ - protected $label = 'Comment'; - - protected $fillable = ['text']; - - public function post() - { - return $this->belongsTo('Vinelab\NeoEloquent\Tests\Functional\QueryingRelations\Post', 'COMMENT'); - } -} diff --git a/tests/functional/SimpleCRUDTest.php b/tests/functional/SimpleCRUDTest.php deleted file mode 100644 index 5e32b042..00000000 --- a/tests/functional/SimpleCRUDTest.php +++ /dev/null @@ -1,399 +0,0 @@ -shouldReceive('connection')->andReturn($this->getConnectionWithConfig('default')); - Wiz::setConnectionResolver($resolver); - } - - public function tearDown(): void - { - M::close(); - - // Mama said, always clean up before you go. =D - $w = Wiz::all(); - $w->each(function ($me) { $me->delete(); }); - - parent::tearDown(); - } - - public function testFindingAndFailing() - { - $this->expectException(ModelNotFoundException::class); - Wiz::findOrFail(0); - } - - /** - * Regression test for issue #27. - * - * @see https://github.com/Vinelab/NeoEloquent/issues/27 - */ - public function testDoesntCrashOnNonIntIds() - { - $u = Wiz::create([]); - $id = (string) $u->id; - $found = Wiz::where('id', "$id")->first(); - $this->assertEquals($found->toArray(), $u->toArray()); - - $foundAgain = Wiz::find("$id"); - $this->assertEquals($foundAgain->toArray(), $u->toArray()); - } - - public function testCreatingRecord() - { - $w = new Wiz(['fiz' => 'foo', 'biz' => 'boo']); - - $this->assertTrue($w->save()); - $this->assertTrue($w->exists); - $this->assertIsInt($w->id); - $this->assertTrue($w->id > 0); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Wiz', $w); - } - - public function testCreatingRecordWithArrayProperties() - { - $w = Wiz::create(['fiz' => ['not', '123', 'helping']]); - - $expected = [ - $w->getKeyName() => $w->getKey(), - 'fiz' => new CypherList(['not', '123', 'helping']), - 'created_at' => $w->created_at->toDateTimeString(), - 'updated_at' => $w->updated_at->toDateTimeString(), - ]; - - $fetched = Wiz::first(); - $this->assertEquals($expected, $fetched->toArray()); - } - - /** - * @depends testCreatingRecord - */ - public function testFindingRecordById() - { - $w = new Wiz(['fiz' => 'foo', 'biz' => 'boo']); - - $this->assertTrue($w->save()); - $this->assertTrue($w->exists); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Wiz', $w); - - $w2 = Wiz::find($w->id); - $this->assertEquals($w->toArray(), $w2->toArray()); - } - - /** - * depends testFindingRecordById. - */ - public function testDeletingRecord() - { - $w = new Wiz(['fiz' => 'foo', 'biz' => 'boo']); - $w->save(); - - $this->assertTrue($w->delete()); - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Wiz', $w); - $this->assertFalse($w->exists); - } - - /** - * @depends testCreatingRecord - */ - public function testMassAssigningAttributes() - { - $w = Wiz::create([ - 'fiz' => 'foo', - 'biz' => 'boo', - 'nope' => 'nope', - ]); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Wiz', $w); - $this->assertTrue($w->exists); - $this->assertIsInt($w->id); - $this->assertNull($w->nope); - } - - /** - * @depends testMassAssigningAttributes - * @depends testFindingRecordById - */ - public function testUpdatingFreshRecord() - { - $w = Wiz::create([ - 'fiz' => 'foo', - 'biz' => 'boo', - ]); - - $found = Wiz::find($w->id); - $this->assertNull($found->nectar, 'make sure it is not there first, just in case some alien invasion put it or something'); - - $w->nectar = 'pulp'; // yummy, freshly saved! - $this->assertTrue($w->save()); - - $after = Wiz::find($w->id); - - $this->assertEquals('pulp', $w->nectar); - $this->assertEquals('pulp', $after->nectar); - } - - /** - * @depends testMassAssigningAttributes - * @depends testFindingRecordById - */ - public function testUpdatingRecordFoundById() - { - $w = Wiz::create([ - 'fiz' => 'foo', - 'biz' => 'boo', - ]); - - $found = Wiz::find($w->id); - $this->assertNull($found->hurry, 'make sure it is not there first, just in case some alien invasion put it or something'); - - $found->hurry = 'up'; - $this->assertTrue($found->save()); - - $after = Wiz::find($w->id); - - $this->assertEquals('up', $found->hurry); - $this->assertEquals('up', $after->hurry); - } - - /** - * Regression test for issue #18 where querying and updating the same - * attributes messes up the values and keeps the old ones resulting in a failed update. - * - * @see https://github.com/Vinelab/NeoEloquent/issues/18 - * - * @return [type] [description] - */ - public function testUpdatingRecordwithUpdateOnQuery() - { - $w = Wiz::create([ - 'fiz' => 'foo', - 'biz' => 'boo', - ]); - - Wiz::where('fiz', '=', 'foo') - ->where('biz', '=', 'boo') - ->update(['fiz' => 'notfooanymore', 'biz' => 'noNotBoo!', 'triz' => 'newhere']); - - $found = Wiz::where('fiz', '=', 'notfooanymore') - ->orWhere('biz', '=', 'noNotBoo!') - ->orWhere('triz', '=', 'newhere') - ->first(); - - $this->assertEquals($w->getKey(), $found->getKey()); - } - - public function testInsertingBatch() - { - $batch = [ - [ - 'fiz' => 'foo', - 'biz' => 'boo', - ], - [ - 'fiz' => 'morefoo', - 'biz' => 'moreboo', - ], - [ - 'fiz' => 'otherfoo', - 'biz' => 'otherboo', - ], - [ - 'fiz' => 'somefoo', - 'biz' => 'someboo', - ], - ]; - - $inserted = Wiz::insert($batch); - - $this->assertTrue($inserted); - - // Let's fetch them to see if that's really true. - $wizzez = Wiz::all(); - - foreach ($wizzez as $key => $wizz) { - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Wiz', $wizz); - $values = $wizz->toArray(); - $this->assertArrayHasKey('id', $values); - $this->assertGreaterThanOrEqual(0, $values['id']); - unset($values['id']); - $this->assertEquals($batch[$key], $values); - } - } - - public function testInsertingSingleAndGettingId() - { - $id = Wiz::insertGetId(['foo' => 'fiz', 'boo' => 'biz']); - - $this->assertIsInt($id); - $this->assertGreaterThan(0, $id, 'message'); - } - - public function testSavingBooleanValuesStayBoolean() - { - $w = Wiz::create(['fiz' => true, 'biz' => false]); - - $g = Wiz::find($w->id); - $this->assertTrue($g->fiz); - $this->assertFalse($g->biz); - } - - public function testNumericValuesPreserveDataTypes() - { - $w = Wiz::create(['fiz' => 1, 'biz' => 8.276123, 'triz' => 0]); - - $g = Wiz::find($w->id); - $this->assertIsInt($g->fiz); - $this->assertIsInt($g->triz); - $this->assertIsFloat($g->biz); - } - - public function testSoftDeletingModel() - { - $w = WizDel::create([]); - - $g = WizDel::all()->first(); - $g->delete(); - $this->assertFalse($g->exists); - $this->assertInstanceOf('Carbon\Carbon', $g->deleted_at); - } - - public function testRestoringSoftDeletedModel() - { - $w = WizDel::create([]); - - $g = WizDel::first(); - $g->delete(); - - $this->assertFalse($g->exists); - $this->assertInstanceOf('Carbon\Carbon', $g->deleted_at); - - $h = WizDel::onlyTrashed()->where('id', $g->getKey())->first(); - $this->assertInstanceOf('Carbon\Carbon', $h->deleted_at); - $this->assertTrue($h->restore()); - $this->assertNull($h->deleted_at); - } - - public function testGettingModelCount() - { - $count = WizDel::count(); - $this->assertEquals(0, $count); - - WizDel::create([]); - $countAfter = WizDel::count(); - $this->assertEquals(1, $countAfter); - } - - public function testFirstOrCreate() - { - $w = Wiz::firstOrCreate([ - 'fiz' => 'foo', - 'biz' => 'boo', - 'triz' => 'troo', - ]); - - $this->assertInstanceOf('Vinelab\NeoEloquent\Tests\Functional\Wiz', $w); - - $found = Wiz::firstOrCreate([ - 'fiz' => 'foo', - 'biz' => 'boo', - 'triz' => 'troo', - ]); - - $this->assertEquals($w->toArray(), $found->toArray()); - } - - public function testCreatingNullAndBooleanValues() - { - $w = Wiz::create([ - 'fiz' => null, - 'biz' => false, - 'triz' => true, - ]); - - $this->assertNotNull($w->getKey()); - - $found = Wiz::where('fiz', '=', null)->where('biz', '=', false)->where('triz', '=', true)->first(); - - $this->assertNull($found->fiz); - $this->assertFalse($found->biz); - $this->assertTrue($found->triz); - } - - public function testUpdatingNullAndBooleanValues() - { - $w = Wiz::create([ - 'fiz' => 'foo', - 'biz' => 'boo', - 'triz' => 'troo', - ]); - - $this->assertNotNull($w->getKey()); - - $updated = Wiz::where('fiz', 'foo')->where('biz', 'boo')->where('triz', 'troo')->update([ - 'fiz' => null, - 'biz' => false, - 'triz' => true, - ]); - - $this->assertGreaterThan(0, $updated); - } - - public function testSavningDateTimeAndCarbonInstances() - { - $now = Carbon::now(); - $dt = new DateTime(); - $w = Wiz::create(['fiz' => $now, 'biz' => $dt]); - - $format = Wiz::getDateFormat(); - - $fetched = Wiz::first(); - $this->assertEquals($now->format(Wiz::getDateFormat()), $fetched->fiz); - $this->assertEquals($now->format(Wiz::getDateFormat()), $fetched->biz); - - $tomorrow = Carbon::now()->addDay(); - $after = Carbon::now()->addDays(2); - - $fetched->fiz = $tomorrow; - $fetched->biz = $after; - $fetched->save(); - - $updated = Wiz::first(); - $this->assertEquals($tomorrow->format(Wiz::getDateFormat()), $updated->fiz); - $this->assertEquals($after->format(Wiz::getDateFormat()), $updated->biz); - } -} diff --git a/tests/functional/WheresTheTest.php b/tests/functional/WheresTheTest.php deleted file mode 100644 index 259f7906..00000000 --- a/tests/functional/WheresTheTest.php +++ /dev/null @@ -1,319 +0,0 @@ -each(function ($u) { $u->delete(); }); - - parent::tearDown(); - } - - public function setUp(): void - { - parent::setUp(); - - $resolver = M::mock('Illuminate\Database\ConnectionResolverInterface'); - $resolver->shouldReceive('connection')->andReturn($this->getConnectionWithConfig('default')); - User::setConnectionResolver($resolver); - - // Setup the data in the database - $this->ab = User::create([ - 'name' => 'Ey Bee', - 'alias' => 'ab', - 'email' => 'ab@alpha.bet', - 'calls' => 10, - ]); - - $this->cd = User::create([ - 'name' => 'See Dee', - 'alias' => 'cd', - 'email' => 'cd@alpha.bet', - 'calls' => 20, - ]); - - $this->ef = User::create([ - 'name' => 'Eee Eff', - 'alias' => 'ef', - 'email' => 'ef@alpha.bet', - 'calls' => 30, - ]); - - $this->gh = User::create([ - 'name' => 'Gee Aych', - 'alias' => 'gh', - 'email' => 'gh@alpha.bet', - 'calls' => 40, - ]); - - $this->ij = User::create([ - 'name' => 'Eye Jay', - 'alias' => 'ij', - 'email' => 'ij@alpha.bet', - 'calls' => 50, - ]); - } - - public function testWhereIdWithNoOperator() - { - $u = User::where('id', $this->ab->id)->first(); - - $this->assertEquals($this->ab->toArray(), $u->toArray()); - } - - public function testWhereIdSelectingProperties() - { - $u = User::where('id', $this->ab->id)->first(['id', 'name', 'email']); - - $this->assertEquals($this->ab->id, $u->id); - $this->assertEquals($this->ab->name, $u->name); - $this->assertEquals($this->ab->email, $u->email); - } - - public function testWhereIdWithEqualsOperator() - { - $u = User::where('id', '=', $this->cd->id)->first(); - - $this->assertEquals($this->cd->toArray(), $u->toArray()); - } - - public function testWherePropertyWithoutOperator() - { - $u = User::where('alias', 'ab')->first(); - - $this->assertEquals($this->ab->toArray(), $u->toArray()); - } - - public function testWherePropertyEqualsOperator() - { - $u = User::where('alias', '=', 'ab')->first(); - - $this->assertEquals($this->ab->toArray(), $u->toArray()); - } - - public function testWhereGreaterThanOperator() - { - $u = User::where('calls', '>', 10)->first(); - $this->assertEquals($this->cd->toArray(), $u->toArray()); - - $others = User::where('calls', '>', 10)->get(); - $this->assertCount(4, $others); - - $brothers = new Collection(array( - $this->cd, - $this->ef, - $this->gh, - $this->ij, )); - $this->assertEquals($others->toArray(), $brothers->toArray()); - - $lastTwo = User::where('calls', '>=', 40)->get(); - $this->assertCount(2, $lastTwo); - - $mothers = new Collection(array($this->gh, $this->ij)); - $this->assertEquals($lastTwo->toArray(), $mothers->toArray()); - - $none = User::where('calls', '>', 9000)->get(); - $this->assertCount(0, $none); - } - - public function testWhereLessThanOperator() - { - $u = User::where('calls', '<', 10)->get(); - $this->assertCount(0, $u); - - $ab = User::where('calls', '<', 20)->first(); - $this->assertEquals($this->ab->toArray(), $ab->toArray()); - - $three = User::where('calls', '<=', 30)->get(); - $this->assertCount(3, $three); - - $cocoa = new Collection(array($this->ab, - $this->cd, - $this->ef, )); - $this->assertEquals($cocoa->toArray(), $three->toArray()); - - $below = User::where('calls', '<', -100)->get(); - $this->assertCount(0, $below); - - $nil = User::where('calls', '<=', 0)->first(); - $this->assertNull($nil); - } - - public function testWhereDifferentThanOperator() - { - $notab = User::where('alias', '<>', 'ab')->get(); - - $dudes = new Collection(array( - $this->cd, - $this->ef, - $this->gh, - $this->ij, )); - - $this->assertCount(4, $notab); - $this->assertEquals($notab->toArray(), $dudes->toArray()); - } - - public function testWhereIn() - { - $alpha = User::whereIn('alias', ['ab', 'cd', 'ef', 'gh', 'ij'])->get(); - - $crocodile = new Collection(array($this->ab, - $this->cd, - $this->ef, - $this->gh, - $this->ij, )); - - $this->assertEquals($alpha->toArray(), $crocodile->toArray()); - } - - public function testWhereNotNull() - { - $alpha = User::whereNotNull('alias')->get(); - - $crocodile = new Collection(array($this->ab, - $this->cd, - $this->ef, - $this->gh, - $this->ij, )); - - $this->assertEquals($alpha->toArray(), $crocodile->toArray()); - } - - public function testWhereNull() - { - $u = User::whereNull('calls')->get(); - $this->assertCount(0, $u); - } - - public function testWhereNotIn() - { - /* - * There is no WHERE NOT IN [ids] in Neo4j, it should be something like this: - * - * MATCH (actor:Actor {name:"Tom Hanks"} )-[:ACTED_IN]->(movies)<-[:ACTED_IN]-(coactor) - * WITH collect(distinct coactor) as coactors - * MATCH (actor:Actor) - * WHERE actor NOT IN coactors - * RETURN actor - */ - $u = User::whereNotIn('alias', ['ab', 'cd', 'ef'])->get(); - $still = new Collection(array($this->gh, $this->ij)); - $rest = [$this->gh->toArray(), $this->ij->toArray()]; - - $this->assertCount(2, $u); - $this->assertEquals($rest, $still->toArray()); - } - - public function testWhereBetween() - { - /* - * There is no WHERE BETWEEN - */ - $this->markTestIncomplete(); - - $u = User::whereBetween('id', [$this->ab->id, $this->ij->id])->get(); - - $mwahaha = new Collection(array($this->ab, - $this->cd, - $this->ef, - $this->gh, - $this->ij, )); - $this->assertCount(5, $u); - $this->assertEquals($buddies->toArray(), $mwahaha->toArray()); - } - - public function testOrWhere() - { - $buddies = User::where('name', 'Ey Bee') - ->orWhere('alias', 'cd') - ->orWhere('email', 'ef@alpha.bet') - ->orWhere('id', $this->gh->id) - ->orWhere('calls', '>', 40) - ->get(); - - $this->assertCount(5, $buddies); - $bigBrothers = new Collection(array($this->ab, - $this->cd, - $this->ef, - $this->gh, - $this->ij, )); - - $this->assertEquals($buddies->toArray(), $bigBrothers->toArray()); - } - - public function testOrWhereIn() - { - $all = User::whereIn('id', [$this->ab->id, $this->cd->id]) - ->orWhereIn('alias', ['ef', 'gh', 'ij'])->get(); - - $padrougas = new Collection(array($this->ab, - $this->cd, - $this->ef, - $this->gh, - $this->ij, )); - $array = $all->toArray(); - usort($array, static fn (array $x, array $y) => $x['id'] <=> $y['id']); - $padrougasArray = $padrougas->toArray(); - usort($padrougasArray, static fn (array $x, array $y) => $x['id'] <=> $y['id']); - $this->assertEquals($array, $padrougasArray); - } - - public function testWhereNotFound() - { - $u = User::where('id', '<', 1)->get(); - $this->assertCount(0, $u); - - $u2 = User::where('glasses', 'always on')->first(); - $this->assertNull($u2); - } - - /** - * Regression test for issue #19. - * - * @see https://github.com/Vinelab/NeoEloquent/issues/19 - */ - public function testWhereMultipleValuesForSameColumn() - { - $u = User::where('alias', '=', 'ab')->orWhere('alias', '=', 'cd')->get(); - $this->assertCount(2, $u); - $this->assertEquals('ab', $u[0]->alias); - $this->assertEquals('cd', $u[1]->alias); - } - - /** - * Regression test for issue #41. - * - * @see https://github.com/Vinelab/NeoEloquent/issues/41 - */ - public function testWhereWithIn() - { - $ab = User::where('alias', 'IN', ['ab'])->first(); - - $this->assertEquals($this->ab->toArray(), $ab->toArray()); - - $users = User::where('alias', 'IN', ['cd', 'ef'])->get(); - - $l = (new User())->getConnection()->getQueryLog(); - - $this->assertEquals($this->cd->toArray(), $users[0]->toArray()); - $this->assertEquals($this->ef->toArray(), $users[1]->toArray()); - } -}