├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist └── src ├── AlgoliaEloquentTrait.php ├── AlgoliaServiceProvider.php ├── EloquentSubscriber.php └── ModelHelper.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | phpunit.xml 4 | *.sublime* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.5.9 5 | - 5.5 6 | - 5.6 7 | - 7.0 8 | - 7.1 9 | - hhvm 10 | 11 | matrix: 12 | fast_finish: true 13 | 14 | sudo: false 15 | 16 | install: 17 | - travis_retry composer install --no-interaction --prefer-source 18 | 19 | script: 20 | - vendor/bin/phpunit $PHPUNIT_FLAGS 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.7.1 (release 2017-01-31) 4 | - Make compatible with Laravel 5.4 5 | 6 | ## 1.1.0 (release 2016-05-03) 7 | 8 | - Add option to dynamically set `autoIndex` and/or `autoDelete` by methods in model. 9 | 10 | ## 1.0.5 (release 2015-10-27) 11 | 12 | - Fix slaves + env 13 | 14 | ## 1.0.3 (release 2015-07-07) 15 | 16 | - Improve Documentation 17 | - Add constistency for trait attributes 18 | - Add browse and browseFrom method 19 | 20 | ## 1.0.2 (release 2015-06-30) 21 | 22 | - Update vinkla/algolia 23 | 24 | ## 1.0.1 (released 2015-06-18) 25 | 26 | - Fix issue in __callStatic of the trait 27 | - Fix issue with static call inside model with static or self keyword 28 | 29 | ## 1.0.0 (released 2015-06-12) 30 | 31 | - Initial stable release 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | Contributions are welcome, and are accepted via pull requests. Please review these guidelines before submitting any pull requests. 4 | 5 | ## Guidelines 6 | 7 | * Please follow the [PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) and [PHP-FIG Naming Conventions](https://github.com/php-fig/fig-standards/blob/master/bylaws/002-psr-naming-conventions.md). 8 | * Ensure that the current tests pass, and if you've added something new, add the tests where relevant. 9 | * Remember that we follow [SemVer](http://semver.org). If you are changing the behaviour, or the public api, you may need to update the docs. 10 | * Send a coherent commit history, making sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash](http://git-scm.com/book/en/Git-Tools-Rewriting-History) them before submitting. 11 | * You may also need to [rebase](http://git-scm.com/book/en/Git-Branching-Rebasing) to avoid merge conflicts. 12 | 13 | 14 | ## Running Tests 15 | 16 | You will need an install of [Composer](https://getcomposer.org) before continuing. 17 | 18 | First, install the dependencies: 19 | 20 | ```bash 21 | $ composer install 22 | ``` 23 | 24 | Then run phpunit: 25 | 26 | ```bash 27 | $ vendor/bin/phpunit 28 | ``` 29 | 30 | If the test suite passes on your local machine you should be good to go. 31 | 32 | When you make a pull request, the tests will automatically be run again by [Travis CI](https://travis-ci.org/) on multiple php versions and hhvm. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Algolia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [DEPRECATED] Algolia Search API Client for Laravel 2 | 3 | [Algolia Search](https://www.algolia.com) is a hosted full-text, numerical, and faceted search engine capable of delivering realtime results from the first keystroke. 4 | 5 | --- 6 | 7 | **This package is deprecated, we recommend you to use [Laravel Scout](https://laravel.com/docs/5.4/scout)**. If you want to extend Scout capabilities, please refer to [our dedicated documentation](https://www.algolia.com/doc/api-client/laravel/algolia-and-scout/) 8 | 9 | --- 10 | 11 | [![Build Status](https://img.shields.io/travis/algolia/algoliasearch-laravel/master.svg?style=flat)](https://travis-ci.org/algolia/algoliasearch-laravel) 12 | [![Latest Version](https://img.shields.io/github/release/algolia/algoliasearch-laravel.svg?style=flat)](https://github.com/algolia/algoliasearch-laravel/releases) 13 | [![License](https://img.shields.io/packagist/l/algolia/algoliasearch-laravel.svg?style=flat)](https://packagist.org/packages/algolia/algoliasearch-laravel) 14 | 15 | 16 | This PHP package integrates the Algolia Search API into the Laravel Eloquent ORM. It's based on the [algoliasearch-client-php](https://github.com/algolia/algoliasearch-client-php) package. 17 | 18 | **Note:** If you're using Laravel 4, checkout the [algoliasearch-laravel-4](https://github.com/algolia/algoliasearch-laravel-4) repository. 19 | 20 | 21 | 22 | 23 | ## API Documentation 24 | 25 | You can find the full reference on [Algolia's website](https://www.algolia.com/doc/api-client/laravel/). 26 | 27 | 28 | ## Table of Contents 29 | 30 | 31 | 1. **[Install](#install)** 32 | 33 | * [Install via composer](#install-via-composer) 34 | * [Service provider](#service-provider) 35 | * [Publish vendor](#publish-vendor) 36 | 37 | 1. **[Quick Start](#quick-start)** 38 | 39 | * [Quick Start](#quick-start) 40 | * [Ranking & Relevance](#ranking--relevance) 41 | * [Frontend Search (realtime experience)](#frontend-search-realtime-experience) 42 | * [Backend Search](#backend-search) 43 | 44 | 1. **[Options](#options)** 45 | 46 | * [Auto-indexing & Asynchronism](#auto-indexing--asynchronism) 47 | * [Custom Index Name](#custom-index-name) 48 | * [Per-environment Indexes](#per-environment-indexes) 49 | * [Custom `objectID`](#custom-objectid) 50 | * [Restrict Indexing to a Subset of Your Data](#restrict-indexing-to-a-subset-of-your-data) 51 | 52 | 1. **[Relationships](#relationships)** 53 | 54 | * [Relationships](#relationships) 55 | 56 | 1. **[Indexing](#indexing)** 57 | 58 | * [Manual Indexing](#manual-indexing) 59 | * [Manual Removal](#manual-removal) 60 | * [Reindexing](#reindexing) 61 | * [Clearing an Index](#clearing-an-index) 62 | 63 | 1. **[Manage indices](#manage-indices)** 64 | 65 | * [Primary/Replica](#primaryreplica) 66 | * [Target Multiple Indexes](#target-multiple-indexes) 67 | 68 | 1. **[Eloquent compatibility](#eloquent-compatibility)** 69 | 70 | * [Eloquent compatibility](#eloquent-compatibility) 71 | * [Compatibility](#compatibility) 72 | 73 | 74 | 75 | 76 | # Install 77 | 78 | 79 | 80 | ## Install via composer 81 | Add `algolia/algoliasearch-laravel` to your `composer.json` file: 82 | 83 | ```bash 84 | composer require algolia/algoliasearch-laravel 85 | ``` 86 | 87 | ## Service provider 88 | Add the service provider to `config/app.php` in the `providers` array. 89 | 90 | ```php 91 | AlgoliaSearch\Laravel\AlgoliaServiceProvider::class 92 | ``` 93 | 94 | ## Publish vendor 95 | 96 | Laravel Algolia requires a connection configuration. To get started, you'll need to publish all vendor assets: 97 | 98 | ```bash 99 | php artisan vendor:publish 100 | ``` 101 | 102 | You can add the ```--provider="Vinkla\Algolia\AlgoliaServiceProvider"``` option to only publish assets of the Algolia package. 103 | 104 | This will create a `config/algolia.php` file in your app that you can modify to set your configuration. Also, make sure you check for changes compared to the original config file after an upgrade. 105 | 106 | 107 | # Quick Start 108 | 109 | 110 | 111 | ## Quick Start 112 | 113 | The following code adds search capabilities to your `Contact` model creating a `Contact` index: 114 | 115 | ```php 116 | use Illuminate\Database\Eloquent\Model; 117 | use AlgoliaSearch\Laravel\AlgoliaEloquentTrait; 118 | 119 | class Contact extends Model 120 | { 121 | use AlgoliaEloquentTrait; 122 | } 123 | ``` 124 | 125 | By default all visible attributes are sent. If you want to send specific attributes you can do something like: 126 | 127 | ```php 128 | use Illuminate\Database\Eloquent\Model; 129 | 130 | class Contact extends Model 131 | { 132 | use AlgoliaEloquentTrait; 133 | 134 | public function getAlgoliaRecord() 135 | { 136 | return array_merge($this->toArray(), [ 137 | 'custom_name' => 'Custom Name' 138 | ]); 139 | } 140 | } 141 | ``` 142 | 143 | After setting up your model, you need to manually do an initial import of your data. You can do this by calling `reindex` on your model class. Using our previous example, this would be: 144 | 145 | ```php 146 | Contact::reindex(); 147 | ``` 148 | 149 | ## Ranking & Relevance 150 | 151 | We provide many ways to configure your index settings to tune the overall relevancy, but the most important ones are the **searchable attributes** and the attributes reflecting the **record popularity**. You can configure them with the following code: 152 | 153 | ```php 154 | use Illuminate\Database\Eloquent\Model; 155 | 156 | class Contact extends Model 157 | { 158 | use AlgoliaEloquentTrait; 159 | 160 | public $algoliaSettings = [ 161 | 'searchableAttributes' => [ 162 | 'id', 163 | 'name', 164 | ], 165 | 'customRanking' => [ 166 | 'desc(popularity)', 167 | 'asc(name)', 168 | ], 169 | ]; 170 | } 171 | ``` 172 | 173 | You can propagate (save) the settings to algolia by using the `setSetting` method: 174 | 175 | ```php 176 | Contact::setSettings(); 177 | ``` 178 | 179 | #### Synonyms 180 | 181 | Synonyms are used to tell the engine about words or expressions that should be considered equal in regard to the textual relevance. 182 | 183 | Our [synonyms API](https://www.algolia.com/doc/relevance/synonyms) has been designed to manage as easily as possible a large set of synonyms for an index and its replicas. 184 | 185 | You can use the synonyms API by adding a `synonyms` in `$algoliaSettings` class property like this: 186 | 187 | ```php 188 | use Illuminate\Database\Eloquent\Model; 189 | 190 | class Contact extends Model 191 | { 192 | use AlgoliaEloquentTrait; 193 | 194 | public $algoliaSettings = [ 195 | 'synonyms' => [ 196 | [ 197 | 'objectID' => 'red-color', 198 | 'type' => 'synonym', 199 | 'synonyms' => ['red', 'another red', 'yet another red'] 200 | ] 201 | ] 202 | ]; 203 | } 204 | ``` 205 | 206 | You can propagate (save) the settings to algolia using the `setSetting` method: 207 | 208 | ```php 209 | Contact::setSettings(); 210 | ``` 211 | 212 | ## Frontend Search (realtime experience) 213 | 214 | Traditional search implementations tend to have search logic and functionality on the backend. This made sense when the search experience consisted of a user entering a search query, executing that search, and then being redirected to a search result page. 215 | 216 | Implementing search on the backend is no longer necessary. In fact, in most cases it is harmful to performance because of the extra network and processing latency. We highly recommend the usage of our [JavaScript API Client](https://github.com/algolia/algoliasearch-client-javascript) issuing all search requests directly from the end user's browser, mobile device, or client. It will reduce the overall search latency while offloading your servers at the same time. 217 | 218 | In your JavaScript code you can do: 219 | 220 | ```js 221 | var client = algoliasearch('ApplicationID', 'Search-Only-API-Key'); 222 | var index = client.initIndex('YourIndexName'); 223 | index.search('something', function(success, hits) { 224 | console.log(success, hits) 225 | }, { hitsPerPage: 10, page: 0 }); 226 | ``` 227 | 228 | ## Backend Search 229 | 230 | You could also use the `search` method, but it's not recommended to implement an instant/realtime search experience from the backend (having a frontend search gives a better user experience): 231 | 232 | ```php 233 | Contact::search('jon doe'); 234 | ``` 235 | 236 | 237 | # Options 238 | 239 | 240 | 241 | ## Auto-indexing & Asynchronism 242 | 243 | Each time a record is saved; it will be - asynchronously - indexed. On the other hand, each time a record is destroyed, it will be - asynchronously - removed from the index. 244 | 245 | You can disable the auto-indexing and auto-removing by setting the following options: 246 | 247 | ```php 248 | use Illuminate\Database\Eloquent\Model; 249 | 250 | class Contact extends Model 251 | { 252 | use AlgoliaEloquentTrait; 253 | 254 | public static $autoIndex = false; 255 | public static $autoDelete = false; 256 | } 257 | ``` 258 | 259 | You can temporarily disable auto-indexing. This is often done for performance reasons. 260 | 261 | ```php 262 | Contact::$autoIndex = false; 263 | Contact::clearIndices(); 264 | 265 | for ($i = 0; $i < 10000; $i++) { 266 | $contact = Contact::firstOrCreate(['name' => 'Jean']); 267 | } 268 | 269 | Contact::reindex(); // Will use batch operations. 270 | Contact::$autoIndex = true; 271 | ``` 272 | 273 | You can also make a dynamic condition for those two parameters by creating an `autoIndex` and/or `autoDelete method` 274 | on your model 275 | 276 | ```php 277 | use Illuminate\Database\Eloquent\Model; 278 | 279 | class Contact extends Model 280 | { 281 | use AlgoliaEloquentTrait; 282 | 283 | public function autoIndex() 284 | { 285 | if (\App::environment() === 'test') { 286 | return false; 287 | } 288 | 289 | return true; 290 | } 291 | 292 | public static autoDelete() 293 | { 294 | if (\App::environment() === 'test') { 295 | return false; 296 | } 297 | 298 | return true; 299 | } 300 | } 301 | ``` 302 | 303 | Be careful to define those two methods in AlgoliaEloquentTrait. 304 | When putting those methods in a parent class they will be "erased" by AlgoliaEloquentTrait if used in a child class 305 | (because of php inheritance). 306 | 307 | ## Custom Index Name 308 | 309 | By default, the index name will be the pluralized class name, e.g. "Contacts". You can customize the index name by using the `$indices` option: 310 | 311 | ```php 312 | use Illuminate\Database\Eloquent\Model; 313 | 314 | class Contact extends Model 315 | { 316 | use AlgoliaEloquentTrait; 317 | 318 | public $indices = ['contact_all']; 319 | } 320 | ``` 321 | 322 | ## Per-environment Indexes 323 | 324 | You can suffix the index name with the current App environment using the following option: 325 | 326 | ```php 327 | use Illuminate\Database\Eloquent\Model; 328 | 329 | class Contact extends Model 330 | { 331 | use AlgoliaEloquentTrait; 332 | 333 | public static $perEnvironment = true; // Index name will be 'Contacts_{\App::environnement()}'; 334 | } 335 | ``` 336 | 337 | ## Custom `objectID` 338 | 339 | By default, the `objectID` is based on your record's `keyName` (`id` by default). You can change this behavior specifying the `objectIdKey` option (be sure to use a uniq field). 340 | 341 | ```php 342 | use Illuminate\Database\Eloquent\Model; 343 | 344 | class Contact extends Model 345 | { 346 | use AlgoliaEloquentTrait; 347 | 348 | public static $objectIdKey = 'new_key'; 349 | } 350 | ``` 351 | 352 | ## Restrict Indexing to a Subset of Your Data 353 | 354 | You can add constraints controlling if a record must be indexed by defining the `indexOnly()` method. 355 | 356 | ```php 357 | use Illuminate\Database\Eloquent\Model; 358 | 359 | class Contact extends Model 360 | { 361 | use AlgoliaEloquentTrait; 362 | 363 | public function indexOnly($index_name) 364 | { 365 | return (bool) $condition; 366 | } 367 | } 368 | ``` 369 | 370 | 371 | # Relationships 372 | 373 | 374 | 375 | ## Relationships 376 | 377 | By default the Algolia package will fetch the **loaded** relationships. 378 | 379 | If you want to index records that haven't yet loaded any relations, you can do it by loading them in the ```getAlgoliaRecord``` that you can create in your model. 380 | 381 | It will look like: 382 | 383 | ```php 384 | public function getAlgoliaRecord() 385 | { 386 | /** 387 | * Load the categories relation so that it's available 388 | * in the laravel toArray method 389 | */ 390 | $this->categories; 391 | 392 | return $this->toArray(); 393 | } 394 | ``` 395 | 396 | In the resulted object, you will have categories converted to array by Laravel. If you want a custom relation structure you will instead do something like: 397 | 398 | ```php 399 | public function getAlgoliaRecord() 400 | { 401 | /** 402 | * Load the categories relation so that it's available 403 | * in the laravel toArray method 404 | */ 405 | $extra_data = []; 406 | $extra_data['categories'] = array_map(function ($data) { 407 | return $data['name']; 408 | }, $this->categories->toArray()); 409 | 410 | return array_merge($this->toArray(), $extra_data); 411 | } 412 | ``` 413 | 414 | 415 | # Indexing 416 | 417 | ## Visibility 418 | 419 | By default, Algolia will only be able to access **visible** attributes of your model. So, for example, you will receive a `No content in PUT request` exception when using this example code, because `invisible_attribute` key returns an empty/null variable. 420 | 421 | ```php 422 | protected $visible = ['visible_attribute', 'other_visible_attribute']; 423 | 424 | public function getAlgoliaRecord() 425 | { 426 | return [ 427 | 'invisible_attribute' => $this->invisible_attribute 428 | ]; 429 | } 430 | ``` 431 | 432 | Before Indexing, be sure to have correctly listed your visible attributes. To bypass this safety mask imposed by Laravel, you may use `$this->attributes['invisible_attribute']` to access directly to the attribute even if is not visible, but the recommendation is to avoid this type of access to attributes in your Model. 433 | 434 | ## Manual Indexing 435 | 436 | You can trigger indexing using the `pushToIndex` instance method. 437 | 438 | ```php 439 | $contact = Contact::firstOrCreate(['name' => 'Jean']); 440 | $contact->pushToIndex(); 441 | ``` 442 | 443 | ## Manual Removal 444 | 445 | And trigger the removal using the `removeFromIndex` instance method. 446 | 447 | ```php 448 | $contact = Contact::firstOrCreate(['name' => 'Jean']); 449 | $contact->removeFromIndex(); 450 | ``` 451 | 452 | ## Reindexing 453 | 454 | To *safely* reindex all your records (index to a temporary index + move the temporary index to the current one atomically), use the `reindex` class method: 455 | 456 | ```php 457 | Contact::reindex(); 458 | ``` 459 | 460 | To reindex all your records (in place, without deleting outdated records): 461 | 462 | ```php 463 | Contact::reindex(false); 464 | ``` 465 | 466 | To set settings during the reindexing process: 467 | 468 | ```php 469 | Contact::reindex(true, true); 470 | ``` 471 | 472 | To keep settings that you set on the Algolia dashboard when reindexing and changing settings: 473 | 474 | ```php 475 | Contact::reindex(true, true, true); 476 | ``` 477 | 478 | To implement a callback that gets called everytime a batch of entities is indexed: 479 | 480 | ```php 481 | Contact::reindex(true, true, false, function ($entities) 482 | { 483 | foreach ($entities as $entity) 484 | { 485 | var_dump($entity->id); // Contact::$id 486 | } 487 | }); 488 | ``` 489 | 490 | ## Clearing an Index 491 | 492 | To clear an index, use the `clearIndices` class method: 493 | 494 | ```ruby 495 | Contact::clearIndices(); 496 | ``` 497 | 498 | # Manage indices 499 | 500 | 501 | 502 | ## Primary/Replica 503 | 504 | You can define replica indexes using the `$algolia_settings` variable: 505 | 506 | ```php 507 | use Illuminate\Database\Eloquent\Model; 508 | 509 | class Contact extends Model 510 | { 511 | use AlgoliaEloquentTrait; 512 | 513 | public $algoliaSettings = [ 514 | 'searchableAttributes' => [ 515 | 'id', 516 | 'name', 517 | ], 518 | 'customRanking' => [ 519 | 'desc(popularity)', 520 | 'asc(name)', 521 | ], 522 | 'replicas' => [ 523 | 'contacts_desc', 524 | ], 525 | ]; 526 | 527 | public $replicasSettings = [ 528 | 'contacts_desc' => [ 529 | 'ranking' => [ 530 | 'desc(name)', 531 | 'typo', 532 | 'geo', 533 | 'words', 534 | 'proximity', 535 | 'attribute', 536 | 'exact', 537 | 'custom' 538 | ] 539 | ] 540 | ]; 541 | } 542 | ``` 543 | 544 | To search using a replica, use the following code: 545 | 546 | ```php 547 | Book::search('foo bar', ['index' => 'contacts_desc']); 548 | ``` 549 | 550 | ## Target Multiple Indexes 551 | 552 | You can index a record in several indexes using the $indices property: 553 | 554 | ```php 555 | use Illuminate\Database\Eloquent\Model; 556 | 557 | class Contact extends Model 558 | { 559 | use AlgoliaEloquentTrait; 560 | 561 | public $indices = [ 562 | 'contact_public', 563 | 'contact_private', 564 | ]; 565 | 566 | public function indexOnly($indexName) 567 | { 568 | if ($indexName == 'contact_public') 569 | return true; 570 | 571 | return $this->private; 572 | } 573 | 574 | } 575 | ``` 576 | 577 | To search using an extra index, use the following code: 578 | 579 | ```php 580 | Book::search('foo bar', ['index' => 'contacts_private']); 581 | ``` 582 | 583 | 584 | # Eloquent compatibility 585 | 586 | 587 | 588 | ## Eloquent compatibility 589 | 590 | Doing: 591 | 592 | ```php 593 | Ad::where('id', $id)->update($attributes); 594 | ``` 595 | 596 | will not trigger anything in the model (so no update will happen in Algolia). This is because it is not an Eloquent call. It is just a convenient way to generate the query hidden behind the model. 597 | 598 | To make this query work with Algolia you need to do it like this: 599 | 600 | ```php 601 | Ad::find($id)->update($attributes); 602 | ``` 603 | 604 | ## Compatibility 605 | 606 | Compatible with 5.x applications 607 | 608 | 609 | 610 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "algolia/algoliasearch-laravel", 3 | "description": "Laravel Algolia extension", 4 | "license": "MIT", 5 | "keywords": [ 6 | "laravel", 7 | "algolia", 8 | "search", 9 | "api" 10 | ], 11 | "abandoned": "laravel/scout", 12 | "require": { 13 | "php": ">=5.5.9", 14 | "vinkla/algolia": "~2.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "~4.0", 18 | "mockery/mockery": "~0.9", 19 | "orchestra/testbench": "3.1.*" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "AlgoliaSearch\\Laravel\\": "src/" 24 | } 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "AlgoliaSearch\\Tests\\": "tests/" 29 | } 30 | }, 31 | "extra": { 32 | "branch-alias": { 33 | "dev-master": "1.0-dev" 34 | } 35 | }, 36 | "minimum-stability": "dev", 37 | "prefer-stable": true 38 | } 39 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | ./tests 20 | 21 | 22 | 23 | 24 | ./src 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/AlgoliaEloquentTrait.php: -------------------------------------------------------------------------------- 1 | getIndices($this); 27 | $indicesTmp = $safe ? $modelHelper->getIndicesTmp($this) : $indices; 28 | 29 | if ($setSettings === true) { 30 | $setToTmpIndices = ($safe === true); 31 | $this->_setSettings($setToTmpIndices, $mergeOldSettings); 32 | } 33 | 34 | static::chunk(100, function ($models) use ($indicesTmp, $modelHelper, $onInsert) { 35 | /** @var \AlgoliaSearch\Index $index */ 36 | foreach ($indicesTmp as $index) { 37 | $records = []; 38 | $recordsAsEntity = []; 39 | 40 | foreach ($models as $model) { 41 | if ($modelHelper->indexOnly($model, $index->indexName)) { 42 | $records[] = $model->getAlgoliaRecordDefault($index->indexName); 43 | 44 | if ($onInsert && is_callable($onInsert)) { 45 | $recordsAsEntity[] = $model; 46 | } 47 | } 48 | } 49 | 50 | $index->addObjects($records); 51 | 52 | if ($onInsert && is_callable($onInsert)) { 53 | call_user_func_array($onInsert, [$recordsAsEntity]); 54 | } 55 | } 56 | 57 | }); 58 | 59 | if ($safe) { 60 | for ($i = 0; $i < count($indices); $i++) { 61 | $modelHelper->algolia->moveIndex($indicesTmp[$i]->indexName, $indices[$i]->indexName); 62 | } 63 | 64 | $this->_setSettings(false); // To a setSettings to set the slave on the master 65 | } 66 | } 67 | 68 | public function _clearIndices() 69 | { 70 | /** @var \AlgoliaSearch\Laravel\ModelHelper $modelHelper */ 71 | $modelHelper = App::make('\AlgoliaSearch\Laravel\ModelHelper'); 72 | 73 | $indices = $modelHelper->getIndices($this); 74 | 75 | /** @var \AlgoliaSearch\Index $index */ 76 | foreach ($indices as $index) { 77 | $index->clearIndex(); 78 | } 79 | } 80 | 81 | /** 82 | * @param $query 83 | * @param array $parameters 84 | * @param $cursor 85 | * 86 | * @return mixed 87 | */ 88 | public function _browseFrom($query, $parameters = [], $cursor = null) 89 | { 90 | /** @var \AlgoliaSearch\Laravel\ModelHelper $modelHelper */ 91 | $modelHelper = App::make('\AlgoliaSearch\Laravel\ModelHelper'); 92 | 93 | $index = null; 94 | 95 | if (isset($parameters['index'])) { 96 | $index = $modelHelper->getIndices($this, $parameters['index'])[0]; 97 | unset($parameters['index']); 98 | } else { 99 | $index = $modelHelper->getIndices($this)[0]; 100 | } 101 | 102 | $result = $index->browseFrom($query, $parameters, $cursor); 103 | 104 | return $result; 105 | } 106 | 107 | /** 108 | * @param $query 109 | * @param array $parameters 110 | * 111 | * @return mixed 112 | */ 113 | public function _browse($query, $parameters = []) 114 | { 115 | /** @var \AlgoliaSearch\Laravel\ModelHelper $modelHelper */ 116 | $modelHelper = App::make('\AlgoliaSearch\Laravel\ModelHelper'); 117 | 118 | $index = null; 119 | 120 | if (isset($parameters['index'])) { 121 | $index = $modelHelper->getIndices($this, $parameters['index'])[0]; 122 | unset($parameters['index']); 123 | } else { 124 | $index = $modelHelper->getIndices($this)[0]; 125 | } 126 | 127 | $result = $index->browse($query, $parameters); 128 | 129 | return $result; 130 | } 131 | 132 | /** 133 | * @param $query 134 | * @param array $parameters 135 | * 136 | * @return mixed 137 | */ 138 | public function _search($query, $parameters = []) 139 | { 140 | /** @var \AlgoliaSearch\Laravel\ModelHelper $modelHelper */ 141 | $modelHelper = App::make('\AlgoliaSearch\Laravel\ModelHelper'); 142 | 143 | $index = null; 144 | 145 | if (isset($parameters['index'])) { 146 | $index = $modelHelper->getIndices($this, $parameters['index'])[0]; 147 | unset($parameters['index']); 148 | } else { 149 | $index = $modelHelper->getIndices($this)[0]; 150 | } 151 | 152 | $result = $index->search($query, $parameters); 153 | 154 | return $result; 155 | } 156 | 157 | public function _setSettings($setToTmpIndices = false, $mergeOldSettings = false) 158 | { 159 | /** @var \AlgoliaSearch\Laravel\ModelHelper $modelHelper */ 160 | $modelHelper = App::make('\AlgoliaSearch\Laravel\ModelHelper'); 161 | 162 | $settings = $modelHelper->getSettings($this); 163 | 164 | if ($setToTmpIndices === false) { 165 | $indices = $modelHelper->getIndices($this); 166 | } 167 | else { 168 | $indices = $modelHelper->getIndicesTmp($this); 169 | } 170 | 171 | $replicas_settings = $modelHelper->getReplicasSettings($this); 172 | $replicas = isset($settings['replicas']) ? $settings['replicas'] : []; 173 | 174 | // Backward compatibility 175 | if ($replicas === [] && isset($settings['slaves'])) { 176 | $replicas = $settings['slaves']; 177 | } 178 | 179 | $b = true; 180 | 181 | /** @var \AlgoliaSearch\Index $index */ 182 | foreach ($indices as $key => $index) { 183 | if ($mergeOldSettings) { 184 | $old_indices = $modelHelper->getIndices($this); 185 | $old_index = $old_indices[$key]; 186 | 187 | try { 188 | $oldSettings = $old_index->getSettings(); 189 | } 190 | catch (\Exception $e) { 191 | $oldSettings = []; 192 | } 193 | 194 | unset($oldSettings['replicas']); 195 | unset($oldSettings['slaves']); 196 | 197 | $newSettings = $oldSettings; 198 | 199 | foreach ($settings as $settingName => $settingValue) { 200 | $newSettings[$settingName] = $settingValue; 201 | } 202 | 203 | $settings = $newSettings; 204 | } 205 | 206 | if ($b && isset($settings['replicas'])) { 207 | $settings['replicas'] = array_map(function ($indexName) use ($modelHelper) { 208 | return $modelHelper->getFinalIndexName($this, $indexName); 209 | }, $settings['replicas']); 210 | } elseif ($b && isset($settings['slaves'])) { 211 | // Backward compatibility 212 | $settings['slaves'] = array_map(function ($indexName) use ($modelHelper) { 213 | return $modelHelper->getFinalIndexName($this, $indexName); 214 | }, $settings['slaves']); 215 | } 216 | 217 | if (isset($settings['synonyms'])) { 218 | $index->batchSynonyms($settings['synonyms'], true, true); 219 | } 220 | else { 221 | // If no synonyms are passed, clear all synonyms from index 222 | $index->clearSynonyms(true); 223 | } 224 | 225 | // If we move the index the setSettings should not contains slave or replica. 226 | if ($setToTmpIndices && $b) { 227 | $b = false; 228 | unset($settings['replicas']); 229 | unset($settings['slaves']); // backward compatibility 230 | } 231 | 232 | if (count(array_keys($settings)) > 0) { 233 | // Synonyms cannot be pushed into "setSettings", it's got rejected from API and throwing exception 234 | // Synonyms cannot be removed directly from $settings var, because then synonym would not be set to other indices 235 | $settingsWithoutSynonyms = $settings; 236 | unset($settingsWithoutSynonyms['synonyms']); 237 | 238 | $index->setSettings($settingsWithoutSynonyms); 239 | } 240 | 241 | if ($b) { 242 | $b = false; 243 | unset($settings['replicas']); 244 | unset($settings['slaves']); // backward compatibility 245 | } 246 | } 247 | 248 | foreach ($replicas as $replica) { 249 | if (isset($replicas_settings[$replica])) { 250 | $index = $modelHelper->getIndices($this, $replica)[0]; 251 | 252 | $s = array_merge($settings, $replicas_settings[$replica]); 253 | unset($s['synonyms']); 254 | 255 | if (count(array_keys($s)) > 0) 256 | $index->setSettings($s); 257 | } 258 | } 259 | } 260 | 261 | /** 262 | * @param $method 263 | * @param $parameters 264 | * @return mixed 265 | */ 266 | public static function __callStatic($method, $parameters) 267 | { 268 | $instance = new static(); 269 | $overload_method = '_'.$method; 270 | 271 | if (method_exists($instance, $overload_method)) { 272 | return call_user_func_array([$instance, $overload_method], $parameters); 273 | } 274 | 275 | return parent::__callStatic($method, $parameters); 276 | } 277 | 278 | /** 279 | * @param $method 280 | * @param $parameters 281 | * @return mixed 282 | * 283 | * Catch static calls call from within a class. Example : static::method(); 284 | */ 285 | public function __call($method, $parameters) 286 | { 287 | $overload_method = '_'.$method; 288 | 289 | if (method_exists($this, $overload_method)) { 290 | return call_user_func_array([$this, $overload_method], $parameters); 291 | } 292 | 293 | return parent::__call($method, $parameters); 294 | } 295 | 296 | /** 297 | * Methods. 298 | */ 299 | public function getAlgoliaRecordDefault($indexName) 300 | { 301 | /** @var \AlgoliaSearch\Laravel\ModelHelper $modelHelper */ 302 | $modelHelper = App::make('\AlgoliaSearch\Laravel\ModelHelper'); 303 | 304 | $record = null; 305 | 306 | if (method_exists($this, self::$methodGetName)) { 307 | $record = $this->{self::$methodGetName}($indexName); 308 | } else { 309 | $record = $this->toArray(); 310 | } 311 | 312 | if (isset($record['objectID']) == false) { 313 | $record['objectID'] = $modelHelper->getObjectId($this); 314 | } 315 | 316 | return $record; 317 | } 318 | 319 | public function pushToIndex() 320 | { 321 | /** @var \AlgoliaSearch\Laravel\ModelHelper $modelHelper */ 322 | $modelHelper = App::make('\AlgoliaSearch\Laravel\ModelHelper'); 323 | 324 | $indices = $modelHelper->getIndices($this); 325 | 326 | /** @var \AlgoliaSearch\Index $index */ 327 | foreach ($indices as $index) { 328 | if ($modelHelper->indexOnly($this, $index->indexName)) { 329 | $index->addObject($this->getAlgoliaRecordDefault($index->indexName)); 330 | } 331 | } 332 | } 333 | 334 | public function removeFromIndex() 335 | { 336 | /** @var \AlgoliaSearch\Laravel\ModelHelper $modelHelper */ 337 | $modelHelper = App::make('\AlgoliaSearch\Laravel\ModelHelper'); 338 | 339 | $indices = $modelHelper->getIndices($this); 340 | 341 | /** @var \AlgoliaSearch\Index $index */ 342 | foreach ($indices as $index) { 343 | $index->deleteObject($modelHelper->getObjectId($this)); 344 | } 345 | } 346 | 347 | public function autoIndex() 348 | { 349 | return (property_exists($this, 'autoIndex') == false || $this::$autoIndex === true); 350 | } 351 | 352 | public function autoDelete() 353 | { 354 | return (property_exists($this, 'autoDelete') == false || $this::$autoDelete === true); 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/AlgoliaServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerManager(); 19 | 20 | Event::subscribe('\AlgoliaSearch\Laravel\EloquentSubscriber'); 21 | } 22 | 23 | private function registerManager() 24 | { 25 | $this->app->register('Vinkla\Algolia\AlgoliaServiceProvider'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/EloquentSubscriber.php: -------------------------------------------------------------------------------- 1 | modelHelper = $modelHelper; 14 | } 15 | 16 | public function saved($eventName, $payload = null) 17 | { 18 | $model = $this->getModelFromParams($eventName, $payload); 19 | 20 | if (!$this->modelHelper->isAutoIndex($model)) { 21 | return true; 22 | } 23 | 24 | /** @var \AlgoliaSearch\Index $index */ 25 | foreach ($this->modelHelper->getIndices($model) as $index) { 26 | if ($this->modelHelper->indexOnly($model, $index->indexName)) { 27 | $index->addObject($this->modelHelper->getAlgoliaRecord($model, $index->indexName), $this->modelHelper->getObjectId($model)); 28 | } elseif ($this->modelHelper->wouldBeIndexed($model, $index->indexName)) { 29 | $index->deleteObject($this->modelHelper->getObjectId($model)); 30 | } 31 | } 32 | 33 | return true; 34 | } 35 | 36 | public function deleted($eventName, $payload = null) 37 | { 38 | $model = $this->getModelFromParams($eventName, $payload); 39 | 40 | if (!$this->modelHelper->isAutoDelete($model)) { 41 | return true; 42 | } 43 | 44 | /** @var \AlgoliaSearch\Index $index */ 45 | foreach ($this->modelHelper->getIndices($model) as $index) { 46 | $index->deleteObject($this->modelHelper->getObjectId($model)); 47 | } 48 | 49 | return true; 50 | } 51 | 52 | /** 53 | * @param string|Model $eventName 54 | * @param array|null $payload 55 | * 56 | * @return Model 57 | */ 58 | private function getModelFromParams($eventName, $payload = null) 59 | { 60 | if($eventName instanceof Model) { 61 | // Laravel < 5.4 62 | return $eventName; 63 | } 64 | 65 | // Laravel >= 5.4 66 | return $payload[0]; 67 | } 68 | 69 | public function subscribe($events) 70 | { 71 | $events->listen('eloquent.saved*', '\AlgoliaSearch\Laravel\EloquentSubscriber@saved'); 72 | $events->listen('eloquent.deleted*', '\AlgoliaSearch\Laravel\EloquentSubscriber@deleted'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ModelHelper.php: -------------------------------------------------------------------------------- 1 | algolia = $algolia; 15 | } 16 | 17 | private function getIndexName(Model $model) 18 | { 19 | return $model->getTable(); 20 | } 21 | 22 | private function hasAlgoliaTrait(Model $class, $autoload = false) 23 | { 24 | $traits = []; 25 | 26 | // Get traits of all parent classes 27 | do { 28 | $traits = array_merge(class_uses($class, $autoload), $traits); 29 | } while ($class = get_parent_class($class)); 30 | 31 | // Get traits of all parent traits 32 | $traitsToSearch = $traits; 33 | while (!empty($traitsToSearch)) { 34 | $newTraits = class_uses(array_pop($traitsToSearch), $autoload); 35 | $traits = array_merge($newTraits, $traits); 36 | $traitsToSearch = array_merge($newTraits, $traitsToSearch); 37 | }; 38 | 39 | foreach ($traits as $trait => $same) { 40 | $traits = array_merge(class_uses($trait, $autoload), $traits); 41 | } 42 | 43 | $traits = array_unique($traits); 44 | 45 | return (isset($traits['AlgoliaSearch\Laravel\AlgoliaEloquentTrait'])); 46 | } 47 | 48 | public function wouldBeIndexed(Model $model, $index_name) 49 | { 50 | if (! method_exists($model, 'indexOnly')) { 51 | return false; 52 | } 53 | 54 | $cloned = clone $model; 55 | 56 | $cloned->setRawAttributes($cloned->getOriginal()); 57 | 58 | return $cloned->indexOnly($index_name) === true; 59 | } 60 | 61 | public function isAutoIndex(Model $model) 62 | { 63 | return ($this->hasAlgoliaTrait($model) && $model->autoIndex()); 64 | } 65 | 66 | public function isAutoDelete(Model $model) 67 | { 68 | return ($this->hasAlgoliaTrait($model) && $model->autoDelete()); 69 | } 70 | 71 | public function getKey(Model $model) 72 | { 73 | return $model->getKey(); 74 | } 75 | 76 | public function indexOnly(Model $model, $index_name) 77 | { 78 | return !method_exists($model, 'indexOnly') || $model->indexOnly($index_name); 79 | } 80 | 81 | public function getObjectId(Model $model) 82 | { 83 | return $model->{$this->getObjectIdKey($model)}; 84 | } 85 | 86 | public function getObjectIdKey(Model $model) 87 | { 88 | return property_exists($model, 'objectIdKey') ? $model::$objectIdKey : $model->getKeyName(); 89 | } 90 | 91 | public function getSettings(Model $model) 92 | { 93 | return property_exists($model, 'algoliaSettings') ? $model->algoliaSettings : []; 94 | } 95 | 96 | public function getReplicasSettings(Model $model) 97 | { 98 | $replicas_settings = property_exists($model, 'replicasSettings') ? $model->replicasSettings : []; 99 | 100 | // Backward compatibility 101 | if ($replicas_settings === [] && property_exists($model, 'slavesSettings')) { 102 | $replicas_settings = $model->slavesSettings; 103 | } 104 | 105 | return $replicas_settings; 106 | } 107 | 108 | public function getSlavesSettings(Model $model) 109 | { 110 | trigger_error("getSlavesSettings was renamed to getReplicasSettings", E_USER_DEPRECATED); 111 | 112 | return $this->getReplicasSettings($model); 113 | } 114 | 115 | public function getFinalIndexName(Model $model, $indexName) 116 | { 117 | $env_suffix = property_exists($model, 'perEnvironment') && $model::$perEnvironment === true ? '_'.\App::environment() : ''; 118 | 119 | return $indexName.$env_suffix; 120 | } 121 | 122 | /** 123 | * @return \AlgoliaSearch\Index 124 | */ 125 | public function getIndices(Model $model, $indexName = null) 126 | { 127 | $indicesName = $this->buildIndices($model, $indexName); 128 | 129 | $indices = array_map(function ($index_name) use ($model) { 130 | return $this->algolia->initIndex($this->getFinalIndexName($model, $index_name)); 131 | }, $indicesName); 132 | 133 | return $indices; 134 | } 135 | 136 | protected function buildIndices(Model $model, $indexName = null) 137 | { 138 | if ($indexName !== null) { 139 | return [$indexName]; 140 | } 141 | 142 | if (method_exists($model, 'indices') && is_array($model->indices())) { 143 | return $model->indices(); 144 | } 145 | 146 | if (property_exists($model, 'indices') && is_array($model->indices)) { 147 | return $model->indices; 148 | } 149 | 150 | return [$this->getIndexName($model)]; 151 | } 152 | 153 | public function getIndicesTmp(Model $model, $indexName = null) 154 | { 155 | $indicesName = $this->buildIndices($model, $indexName); 156 | 157 | $indices = array_map(function ($index_name) use ($model) { 158 | return $this->algolia->initIndex($this->getFinalIndexName($model, $index_name).'_tmp'); 159 | }, $indicesName); 160 | 161 | return $indices; 162 | } 163 | 164 | public function getAlgoliaRecord(Model $model, $indexName) 165 | { 166 | return $model->getAlgoliaRecordDefault($indexName); 167 | } 168 | } 169 | --------------------------------------------------------------------------------