├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Console │ ├── AlgoliaCommand.php │ ├── BackupCommand.php │ └── PushCommand.php ├── IndexResourceRepository.php └── ServiceProvider.php └── tests ├── Algolia └── Settings │ ├── Console │ ├── BackupCommandTest.php │ └── PushCommandTest.php │ └── IndexResourceRepositoryTest.php ├── TestModel.php └── TestModelWithSearchableTrait.php /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | - Algolia Client Version: #.#.# 15 | - Language Version: 16 | 17 | ### Description 18 | 19 | 20 | ### Steps To Reproduce 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | | Q | A 2 | | ----------------- | ---------- 3 | | Bug fix? | yes/no 4 | | New feature? | yes/no 5 | | BC breaks? | no 6 | | Related Issue | Fix #... 7 | | Need Doc update | yes/no 8 | 9 | 10 | ## Describe your change 11 | 12 | 16 | 17 | ## What problem is this fixing? 18 | 19 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /build 3 | /vendor 4 | /coverage 5 | composer.lock 6 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.1 4 | 5 | * Add compatibility with Laravel Scout 5.0 6 | 7 | ## 2.0.0 8 | 9 | * Better message in console 10 | * Do not use prefix in file names 11 | 12 | In a typical workflow, you would use a different `scout.prefix` for 13 | each environment. Generally, you work on dev env, tweak settings in the 14 | dashboard, save them to your project with `php artisan algolia:settings:backup` 15 | and push them to your new index with you go to prod env. 16 | Because in v1 we would link all resources to the full index, if you 17 | use a different prefix, you weren't able to do this. 18 | In case you want to keep the behavior like in v1, pass the `--prefix` option 19 | to the 2 commands. 20 | 21 | See [issue #14](https://github.com/algolia/laravel-scout-settings/issues/14) 22 | 23 | * Introduce new IndexResourceRepository service class 24 | 25 | This service allows you to read and write files with your settings, 26 | synonyms, rules via simple methods. This class doesn't interact with 27 | Algolia's API but with files in your `resources` folder. 28 | Thanks @qrazi 29 | 30 | 31 | ### 1.0.1 32 | 33 | * Move UserAgent definition to ServiceProvider 34 | * Add compatibility with Scout ^4.0 (Thanks to @tomcoonen) 35 | 36 | ## 1.0.0 37 | 38 | * Initial release 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Hi there! We're thrilled that you'd like to contribute to this project. 4 | Your help is essential to keeping it great. 5 | 6 | ### Opening an issue 7 | 8 | Each repository provides a template for issues. Please tell us the client and language version, and 9 | provide a clear description of the problem you're facing. Steps to reproduce, or example code 10 | (repository, jsfiddle, and such), are a big help. 11 | 12 | ### Submitting a pull request 13 | 14 | Keep your changes as focused as possible. If there are multiple changes you'd like to make, 15 | please consider submitting them as separate pull requests unless they are related to each other. 16 | 17 | Here are a few tips to increase the likelihood of being merged: 18 | 19 | - [ ] Write tests. 20 | - [ ] Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 21 | - [ ] Allow [edits from maintainers](https://blog.github.com/2016-09-07-improving-collaboration-with-forks/). 22 | 23 | ### Security issues 24 | If you find any security risk in the project, please open an issue. 25 | 26 | ### API Breaking changes 27 | 28 | We care deeply about backward compatibility for our API clients libraries. 29 | If it's necessary, we're ready to break backward compatibility, 30 | but this should be pretty rare. 31 | 32 | If you want to make a change that will break the backward compatibility of the API, 33 | open an issue first to discuss it with the maintainers. 34 | 35 | ### Editing `README.md` and similar files 36 | 37 | Note that some files are managed outside this repository and are committed automatically. 38 | 39 | The `README.md` is generated automatically from our doc. If you'd like us to update this file, 40 | feel free to open an issue. 41 | 42 | `.github` directory is managed in [this repository](https://github.com/algolia/algoliasearch-client-common), 43 | any Pull Request there is welcome. 44 | 45 | ## Label caption 46 | 47 | Labels across all Algolia API clients repositories are normalized. 48 | 49 | 50 | 51 | | Label | Meaning | 52 | |---------------------------------------------------------------------------|----------------------------------------------------------------------------------------| 53 | | ![#050f2c](https://placehold.it/15/050f2c/000000?text=+) Do not merge | PR should not be merged (decided by maintainers) | 54 | | ![#ffc168](https://placehold.it/15/ffc168/000000?text=+) WIP | PR is not ready, no need to look at it (decided by contributors) | 55 | | ![#2ede98](https://placehold.it/15/2ede98/000000?text=+) Ready | The PR is ready, reviewed, tests are green, if you're brave enough: click merge button | 56 | | ![#ffc168](https://placehold.it/15/ffc168/000000?text=+) Waiting for API | The feature is implemented but the REST API is not live yet | 57 | | ![#3369e6](https://placehold.it/15/3369e6/000000?text=+) Discussion | We need everyone's opinion on this, please join the conversation and share your ideas | 58 | | ![#3369e6](https://placehold.it/15/3369e6/000000?text=+) Support | A user needs help but it's not really a bug | 59 | | ![#3369e6](https://placehold.it/15/3369e6/000000?text=+) API feature | New API feature added to every client (like query rules) | 60 | | ![#3369e6](https://placehold.it/15/3369e6/000000?text=+) Chore | CI, docs, and everything around the code | 61 | | ![#ff4f81](https://placehold.it/15/ff4f81/000000?text=+) Bug | It's a bug, fix it! | 62 | | ![#b60205](https://placehold.it/15/b60205/000000?text=+) Breaking change | RED ALERT! This means we need a new major version | 63 | | ![#ff6c5f](https://placehold.it/15/ff6c5f/000000?text=+) Good First Issue | If you want to contribute, this one is _easy_ to tackle! | 64 | 65 | 66 | 67 | 68 | ## Resources 69 | 70 | - [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/) 71 | - [Using Pull Requests](https://help.github.com/articles/using-pull-requests/) 72 | - [GitHub Help](https://help.github.com) 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-Present 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 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | ## `algolia/laravel-scout-settings` maintainers 2 | 3 | | Name | Email | 4 | |-----------------|-----------------------------| 5 | | Julien Bourdeau | julien.bourdeau@algolia.com | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **DEPRECATED: Use of this repository is deprecated. Please use Scout Extended - https://github.com/algolia/scout-extended instead.** 2 | 3 | # Laravel Scout Settings 4 | 5 | **Import/Export Algolia settings, synonyms and query rules into your Laravel Scout project.** 6 | 7 | The easiest way to manage your settings is usually to go to your Algolia dashboard because it 8 | has a nice UI and you can test the relevancy directly there. 9 | 10 | Once you fine tuned your configuration, you may want to add it to your project. 11 | 12 | This package adds two Laravel commands to your project: 13 | 14 | - one to save your settings, synonyms and query rules into JSON files 15 | - one to push everything back to Algolia 16 | 17 | This has 3 major advantages: 18 | 19 | 1. You can version your configuration with your VCS 20 | 2. You can set up a new environment or restore backups easily 21 | 3. It lets you customize your settings in JSON format before pushing them 22 | 23 | ## Install 24 | 25 | Install this package with composer 26 | 27 | ```bash 28 | composer require algolia/laravel-scout-settings 29 | ``` 30 | 31 | #### Laravel 5.5 32 | 33 | If you use Laravel 5.5, this package will take advantage of the 34 | [Package Auto-Discovery](https://medium.com/@taylorotwell/package-auto-discovery-in-laravel-5-5-ea9e3ab20518) feature. 35 | Nothing more to do to register the commands. 36 | 37 | #### Laravel 5.4 and prior 38 | 39 | If you use an older version of Laravel, you will have to add the Service Provider to 40 | the `providers` array in `config/app.php` 41 | 42 | ```php 43 | Algolia\Settings\ServiceProvider::class, 44 | ``` 45 | 46 | ## Usage 47 | 48 | You will now get two new commands available in `artisan`. They both take a model's fully 49 | qualified class name, just like Laravel Scout does to import/flush data. 50 | 51 | The following example assume you have an `App\Contact` class, which uses the `Searchable` trait. 52 | 53 | Note: Scout allows you to customize the index name with the 54 | [`searchableAs()`](https://laravel.com/docs/scout#configuring-model-indexes) method. This package 55 | will follow this naming convention. 56 | 57 | ### Backing up settings (Project ⬅️ Algolia) 58 | 59 | The following command will export all the settings and synonyms from the `App\Contact`'s 60 | index into the following files: 61 | 62 | * **Settings**: `resources/algolia-settings/index_name.json` 63 | * **Synonyms**: `resources/algolia-settings/index_name-synonyms.json` 64 | * **Query Rules**: `resources/algolia-settings/index_name-rules` 65 | 66 | ``` 67 | php artisan algolia:settings:backup "App\Contact" 68 | ``` 69 | 70 | Note that if you want to add the prefix to your file names (which was the default behavior in v1), 71 | you can pass the `--prefix` option. 72 | 73 | ``` 74 | php artisan algolia:settings:backup "App\Contact" --prefix 75 | ``` 76 | 77 | ### Pushing settings (Project ➡️ Algolia) 78 | 79 | The following command will read all the settings, synonyms and query rules from the 80 | files in `resources/algolia-settings/` and import them into Algolia's index. 81 | 82 | ``` 83 | php artisan algolia:settings:push "App\Contact" 84 | ``` 85 | 86 | You can also pass the `--prefix` option, just like the backup command. 87 | 88 | ### Customizing directory 89 | 90 | By default, settings, rules and synonyms are saved into the `resources/algolia-settings`. 91 | The directory can be customized by the defining an environment variable named `ALGOLIA_SETTINGS_FOLDER`. 92 | For example, the following command will save all the index resources into `resources/indexmeta`. 93 | 94 | ``` 95 | ALGOLIA_SETTINGS_FOLDER=indexmeta php artisan algolia:settings:backup 96 | ``` 97 | 98 | ## Testing 99 | 100 | ``` bash 101 | composer test 102 | ``` 103 | 104 | ## Need help? 105 | 106 | Feel free to open a thread on our [Community forum](https://discourse.algolia.com/) 107 | 108 | ### Contribute 109 | 110 | Contributions are welcome! 111 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "algolia/laravel-scout-settings", 3 | "description": "Import/Export Algolia settings into your Laravel Scout project", 4 | "keywords": [ 5 | "algolia", 6 | "laravel", 7 | "search" 8 | ], 9 | "type": "library", 10 | "minimum-stability": "stable", 11 | "require": { 12 | "laravel/scout": ">=3.0", 13 | "algolia/algoliasearch-client-php": "^1.25" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Algolia\\Settings\\": "src/" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "Algolia\\Settings\\Tests\\": "tests" 23 | } 24 | }, 25 | "license": "MIT", 26 | "authors": [ 27 | { 28 | "name": "Julien Bourdeau", 29 | "email": "julien.bourdeau@algolia.com" 30 | } 31 | ], 32 | "scripts": { 33 | "test": "vendor/bin/phpunit", 34 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 35 | }, 36 | "extra": { 37 | "laravel": { 38 | "providers": [ 39 | "Algolia\\Settings\\ServiceProvider" 40 | ] 41 | } 42 | }, 43 | "require-dev": { 44 | "phpunit/phpunit": "^6.5", 45 | "mikey179/vfsstream": "^1.6", 46 | "orchestra/testbench": "^3.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Console/AlgoliaCommand.php: -------------------------------------------------------------------------------- 1 | indexRepository = $indexRepository; 19 | } 20 | 21 | protected function getIndex($indexName) 22 | { 23 | return app(Client::class)->initIndex($indexName); 24 | } 25 | 26 | protected function isClassSearchable($fqn) 27 | { 28 | if (! \in_array(Searchable::class, class_uses_recursive($fqn), true)) { 29 | return false; 30 | } 31 | return true; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Console/BackupCommand.php: -------------------------------------------------------------------------------- 1 | argument('model'); 31 | 32 | if (! $this->isClassSearchable($fqn)) { 33 | $this->warn('The class [' . $fqn . '] does not use the [' . Searchable::class . '] trait'); 34 | 35 | // Return value >0 to indicate error. Bash "1" means "general error" 36 | return 1; 37 | } 38 | 39 | if ($usePrefix = $this->option('prefix')) { 40 | $this->indexRepository->usePrefix($usePrefix); 41 | $this->warn('All resources will be saved in files prefixed with '.config('scout.prefix')); 42 | } 43 | 44 | $indexName = (new $fqn)->searchableAs(); 45 | 46 | $success = $this->saveSettings($indexName); 47 | 48 | if ($success) { 49 | $this->info('All settings for ['.$fqn.'] index have been backed up.'); 50 | } else { 51 | $this->warn('The settings could not be saved'); 52 | } 53 | 54 | $success = $this->saveSynonyms($indexName); 55 | 56 | if ($success) { 57 | $this->info('All synonyms for ['.$fqn.'] index have been backed up.'); 58 | } else { 59 | $this->warn('The synonyms could not be saved'); 60 | } 61 | 62 | $success = $this->saveRules($indexName); 63 | 64 | if ($success) { 65 | $this->info('All query rules for ['.$fqn.'] index have been backed up.'); 66 | } else { 67 | $this->warn('The query rules could not be saved'); 68 | } 69 | } 70 | 71 | protected function saveSettings($indexName) 72 | { 73 | $settings = $this->getIndex($indexName)->getSettings(); 74 | 75 | $child = true; 76 | if (isset($settings['replicas'])) { 77 | foreach ($settings['replicas'] as $replica) { 78 | $child = $this->saveSettings($replica); 79 | } 80 | } 81 | 82 | $success = $this->indexRepository->saveSettings($indexName, $settings); 83 | 84 | if ($success) { 85 | $this->line("Saved settings for $indexName"); 86 | } 87 | 88 | return $success && $child; 89 | } 90 | 91 | protected function saveSynonyms($indexName) 92 | { 93 | $synonymIterator = $this->getIndex($indexName)->initSynonymIterator(); 94 | 95 | $synonyms = []; 96 | foreach ($synonymIterator as $synonym) { 97 | $synonyms[] = $synonym; 98 | } 99 | 100 | $success = $this->indexRepository->saveSynonyms($indexName, $synonyms); 101 | 102 | if ($success) { 103 | $this->line("Saved synonyms for $indexName"); 104 | } 105 | 106 | return $success; 107 | } 108 | 109 | protected function saveRules($indexName) 110 | { 111 | $ruleIterator = $this->getIndex($indexName)->initRuleIterator(); 112 | 113 | $rules = []; 114 | foreach ($ruleIterator as $rule) { 115 | $rules[] = $rule; 116 | } 117 | 118 | $success = $this->indexRepository->saveRules($indexName, $rules); 119 | 120 | if ($success) { 121 | $this->line("Saved query rules for $indexName"); 122 | } 123 | 124 | return $success; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Console/PushCommand.php: -------------------------------------------------------------------------------- 1 | argument('model'); 31 | 32 | if (! $this->isClassSearchable($fqn)) { 33 | $this->warn('The class [' . $fqn . '] does not use the [' . Searchable::class . '] trait'); 34 | 35 | return 1; // Return value >0 to indicate error. Bash "1" means "general error" 36 | } 37 | 38 | if ($usePrefix = $this->option('prefix')) { 39 | $this->indexRepository->usePrefix($usePrefix); 40 | $this->warn('All resources will be saved in files prefixed with '.config('scout.prefix')); 41 | } 42 | 43 | $indexName = (new $fqn)->searchableAs(); 44 | 45 | $done = $this->pushSettings($indexName); 46 | 47 | if ($done) { 48 | $this->info('All settings for ['.$fqn.'] index have been pushed.'); 49 | } 50 | 51 | $done = $this->pushSynonyms($indexName); 52 | 53 | if ($done) { 54 | $this->info('All synonyms for ['.$fqn.'] index have been pushed.'); 55 | } 56 | 57 | $done = $this->pushRules($indexName); 58 | 59 | if ($done) { 60 | $this->info('All query rules for ['.$fqn.'] index have been pushed.'); 61 | } 62 | } 63 | 64 | protected function pushSettings($indexName) 65 | { 66 | $settings = $this->indexRepository->getSettings($indexName); 67 | 68 | if (! $settings) { 69 | $this->warn('No settings to push to '.$indexName); 70 | return false; 71 | } 72 | 73 | $this->line('Pushing settings for ' . $indexName . ' index.'); 74 | 75 | if (isset($settings['replicas'])) { 76 | foreach ($settings['replicas'] as $replica) { 77 | $this->pushSettings($replica); 78 | } 79 | } 80 | 81 | $this->getIndex($indexName)->setSettings($settings); 82 | 83 | return true; 84 | } 85 | 86 | protected function pushSynonyms($indexName) 87 | { 88 | $synonyms = $this->indexRepository->getSynonyms($indexName); 89 | 90 | if (! $synonyms) { 91 | $this->warn('No synonyms to push to '.$indexName); 92 | return false; 93 | } 94 | 95 | $index = $this->getIndex($indexName); 96 | // Clear all synonyms from the index 97 | $task = $index->clearSynonyms(true); 98 | $index->waitTask($task['taskID']); 99 | 100 | $this->line('Pushing synonyms for ' . $indexName . ' index.'); 101 | 102 | foreach (array_chunk($synonyms, 1000) as $batch) { 103 | $index->batchSynonyms($batch, true, true); 104 | } 105 | 106 | return true; 107 | } 108 | 109 | protected function pushRules($indexName) 110 | { 111 | $rules = $this->indexRepository->getRules($indexName); 112 | 113 | if (! $rules) { 114 | $this->warn('No query rules to push to '.$indexName); 115 | return false; 116 | } 117 | 118 | $index = $this->getIndex($indexName); 119 | // Clear all rules from the index 120 | $task = $index->clearRules(true); 121 | $index->waitTask($task['taskID']); 122 | 123 | $this->line('Pushing rules for ' . $indexName . ' index.'); 124 | 125 | foreach (array_chunk($rules, 1000) as $batch) { 126 | $index->batchRules($batch, true, true); 127 | } 128 | 129 | return true; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/IndexResourceRepository.php: -------------------------------------------------------------------------------- 1 | getBasePath())) { 16 | File::makeDirectory($path); 17 | } 18 | } 19 | 20 | public function usePrefix($usePrefix = true) 21 | { 22 | $this->usePrefix = $usePrefix; 23 | } 24 | 25 | public function getSettings($indexName) 26 | { 27 | return $this->getJsonFromFile($this->getFilePath($indexName, 'settings')); 28 | } 29 | 30 | public function saveSettings($indexName, $settings) 31 | { 32 | return $this->saveJsonFile($this->getFilePath($indexName, 'settings'), $settings); 33 | } 34 | 35 | public function getSynonyms($indexName) 36 | { 37 | return $this->getJsonFromFile($this->getFilePath($indexName, 'synonyms')); 38 | } 39 | 40 | public function saveSynonyms($indexName, $synonyms) 41 | { 42 | return $this->saveJsonFile($this->getFilePath($indexName, 'synonyms'), $synonyms); 43 | } 44 | 45 | public function getRules($indexName) 46 | { 47 | return $this->getJsonFromFile($this->getFilePath($indexName, 'rules')); 48 | } 49 | 50 | public function saveRules($indexName, $rules) 51 | { 52 | return $this->saveJsonFile($this->getFilePath($indexName, 'rules'), $rules); 53 | } 54 | 55 | private function getJsonFromFile($path) 56 | { 57 | if (! File::exists($path)) { 58 | return []; 59 | } 60 | 61 | return Json::decode( 62 | File::get($path), 63 | true 64 | ); 65 | } 66 | 67 | private function saveJsonFile($path, $json) 68 | { 69 | return File::put( 70 | $path, 71 | Json::encode($json, JSON_PRETTY_PRINT) 72 | ); 73 | } 74 | 75 | private function getFilePath($indexName, $type) 76 | { 77 | if (! $this->usePrefix) { 78 | $indexName = $this->removePrefix($indexName); 79 | } 80 | 81 | $path = $this->getBasePath() . $indexName . $this->normalizeType($type) . '.json'; 82 | 83 | return $path; 84 | } 85 | 86 | private function normalizeType($type) 87 | { 88 | if ('settings' === $type) { 89 | return ''; 90 | } 91 | 92 | return '-' . $type; 93 | } 94 | 95 | private function getBasePath() 96 | { 97 | return rtrim( 98 | app()->resourcePath( 99 | env('ALGOLIA_SETTINGS_FOLDER', 'algolia-settings/') 100 | ), 101 | '/' 102 | ) . DIRECTORY_SEPARATOR; 103 | } 104 | 105 | private function removePrefix($indexName) 106 | { 107 | return preg_replace('/^'.preg_quote(config('scout.prefix'), '/').'/', '', $indexName); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | commands([ 29 | PushCommand::class, 30 | BackupCommand::class, 31 | ]); 32 | 33 | // Service class is stateless so instantiate once 34 | $this->app->singleton(IndexResourceRepository::class, IndexResourceRepository::class); 35 | 36 | $this->app->bind(Client::class, function (Application $application) { 37 | return new Client( 38 | $application->make('config')->get('scout.algolia.id'), 39 | $application->make('config')->get('scout.algolia.secret') 40 | ); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Algolia/Settings/Console/BackupCommandTest.php: -------------------------------------------------------------------------------- 1 | [] 26 | ]; 27 | 28 | // setup and cache the virtual file system 29 | $this->file_system = vfsStream::setup('root', null, $structure); 30 | 31 | $this->app->setBasePath($this->file_system->url()); 32 | } 33 | 34 | public function testHandleSearchableModel() 35 | { 36 | $this->app[Kernel::class]->registerCommand(new BackupCommand(new IndexResourceRepository())); 37 | 38 | $this->app->bind(Client::class, function () { 39 | $clientProphet = $this->prophesize(Client::class); 40 | 41 | $postIndexProphet = $this->prophesize(Index::class); 42 | $postIndexProphet->getSettings()->willReturn([ 43 | 'searchableAttributes' => [ 44 | 'title' 45 | ], 46 | 'replicas' => [ 47 | 'posts_newest' 48 | ], 49 | 'customRanking' => null, 50 | ]); 51 | $postIndexProphet->initSynonymIterator()->willReturn([ 52 | [ 53 | 'type' => 'synonym', 54 | 'synonyms' => [ 55 | 'foo', 56 | 'bar', 57 | 'fooz', 58 | 'baz', 59 | ], 60 | 'objectID' => '1520518966426', 61 | ], 62 | ]); 63 | $postIndexProphet->initRuleIterator()->willReturn([ 64 | [ 65 | 'condition' => [ 66 | 'pattern' => '{facet:prefered_contact}', 67 | 'anchoring' => 'contains', 68 | 'context' => 'e-commerce', 69 | ], 70 | 'consequence' => [ 71 | 'promote' => [ 72 | [ 73 | 'objectID' => '99', 74 | 'position' => 0 75 | ] 76 | ] 77 | ], 78 | 'description' => 'Foo bar' 79 | ] 80 | ]); 81 | 82 | $clientProphet->initIndex('posts')->willReturn($postIndexProphet->reveal()); 83 | 84 | $postNewestIndexProphet = $this->prophesize(Index::class); 85 | $postNewestIndexProphet->getSettings()->willReturn([ 86 | 'searchableAttributes' => [ 87 | 'title' 88 | ], 89 | 'primary' => 'posts', 90 | 'customRanking' => [ 91 | 'desc(published_at)' 92 | ], 93 | ]); 94 | $clientProphet->initIndex('posts_newest')->willReturn($postNewestIndexProphet->reveal()); 95 | 96 | return $clientProphet->reveal(); 97 | }); 98 | 99 | $this->artisan('algolia:settings:backup', ['model' => TestModelWithSearchableTrait::class]); 100 | 101 | $this->assertFileExists($this->file_system->url() . '/resources/algolia-settings/posts.json'); 102 | $this->assertFileExists($this->file_system->url() . '/resources/algolia-settings/posts_newest.json'); 103 | $this->assertFileExists($this->file_system->url() . '/resources/algolia-settings/posts-rules.json'); 104 | $this->assertFileExists($this->file_system->url() . '/resources/algolia-settings/posts-synonyms.json'); 105 | } 106 | 107 | public function testHandleNonSearchableModel() 108 | { 109 | $this->app[Kernel::class]->registerCommand(new BackupCommand(new IndexResourceRepository())); 110 | 111 | $return_code = $this->artisan('algolia:settings:backup', ['model' => TestModel::class]); 112 | $cli_output = $this->app[Kernel::class]->output(); 113 | 114 | $this->assertEquals(1, $return_code); 115 | $this->assertEquals("The class [Algolia\Settings\Tests\TestModel] does not use the [Laravel\Scout\Searchable] trait\n", $cli_output); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/Algolia/Settings/Console/PushCommandTest.php: -------------------------------------------------------------------------------- 1 | [ 27 | 'algolia-settings' => [ 28 | 'posts.json' => json_encode([ 29 | 'searchableAttributes' => [ 30 | 'title' 31 | ], 32 | 'replicas' => [ 33 | 'posts_newest' 34 | ], 35 | 'customRanking' => null, 36 | ]), 37 | 'posts_newest.json' => json_encode([ 38 | 'searchableAttributes' => [ 39 | 'title' 40 | ], 41 | 'primary' => 'posts', 42 | 'customRanking' => [ 43 | 'desc(published_at)' 44 | ], 45 | ]), 46 | 'posts-rules.json' => json_encode([ 47 | [ 48 | 'objectID' => 'a-rule-id', 49 | 'condition' => [ 50 | 'pattern' => 'smartphone', 51 | 'anchoring' => 'contains' 52 | ], 53 | 'consequence' => [ 54 | 'params' => [ 55 | 'filters' => 'category = 1', 56 | ], 57 | ], 58 | ], 59 | ]), 60 | 'posts-synonyms.json' => json_encode([ 61 | [ 62 | 'objectID' => 'a-unique-identifier', 63 | 'type' => 'synonym', 64 | 'synonyms' => ['car', 'vehicle', 'auto'], 65 | ], 66 | ]), 67 | ], 68 | ], 69 | ]; 70 | 71 | // setup and cache the virtual file system 72 | $this->file_system = vfsStream::setup('root', null, $structure); 73 | 74 | $this->app->setBasePath($this->file_system->url()); 75 | } 76 | 77 | public function testHandleSearchableModel() 78 | { 79 | $this->app[Kernel::class]->registerCommand(new PushCommand(new IndexResourceRepository())); 80 | $this->app->bind(Client::class, function () { 81 | $clientProphet = $this->prophesize(Client::class); 82 | 83 | $postIndexProphet = $this->prophesize(Index::class); 84 | 85 | $clientProphet->initIndex(Argument::any())->willReturn($postIndexProphet->reveal()); 86 | 87 | return $clientProphet->reveal(); 88 | }); 89 | 90 | $return_code = $this->artisan('algolia:settings:push', ['model' => TestModelWithSearchableTrait::class]); 91 | 92 | $this->assertEquals(0, $return_code); 93 | } 94 | 95 | public function testHandleNonSearchableModel() 96 | { 97 | $this->app[Kernel::class]->registerCommand(new PushCommand(new IndexResourceRepository())); 98 | 99 | $return_code = $this->artisan('algolia:settings:push', ['model' => TestModel::class]); 100 | $cli_output = $this->app[Kernel::class]->output(); 101 | 102 | $this->assertEquals(1, $return_code); 103 | $this->assertEquals( 104 | "The class [Algolia\Settings\Tests\TestModel] does not use the [Laravel\Scout\Searchable] trait\n", 105 | $cli_output 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/Algolia/Settings/IndexResourceRepositoryTest.php: -------------------------------------------------------------------------------- 1 | [ 26 | 'algolia-settings' => [ 27 | 'posts.json' => <<<'POST' 28 | { 29 | "searchableAttributes": [ 30 | "title" 31 | ], 32 | "replicas": [ 33 | "posts_newest", 34 | "posts_oldest" 35 | ], 36 | "customRanking": null 37 | } 38 | POST 39 | , 40 | 'posts_newest' => <<<'POST' 41 | { 42 | "searchableAttributes": [ 43 | "title" 44 | ], 45 | "primary": "posts", 46 | "customRanking": [ 47 | "desc(published_at)" 48 | ] 49 | } 50 | POST 51 | , 52 | 'posts_oldest' => <<<'POST' 53 | { 54 | "searchableAttributes": [ 55 | "title" 56 | ], 57 | "primary": "posts", 58 | "customRanking": [ 59 | "asc(published_at)" 60 | ] 61 | } 62 | POST 63 | , 64 | ], 65 | 'custom-sub-path-settings' => [ 66 | 'posts.json' => <<<'POST' 67 | { 68 | "searchableAttributes": [ 69 | "title", 70 | "summary" 71 | ] 72 | } 73 | POST 74 | , 75 | ], 76 | ], 77 | ]; 78 | 79 | // setup and cache the virtual file system 80 | $this->file_system = vfsStream::setup('root', null, $structure); 81 | 82 | $this->app->setBasePath($this->file_system->url()); 83 | } 84 | 85 | public function testGetSettingsDefaultLocation() 86 | { 87 | $sut = new IndexResourceRepository(); 88 | 89 | $expected = [ 90 | 'title' 91 | ]; 92 | 93 | $postSettings = $sut->getSettings((new TestModelWithSearchableTrait)->searchableAs()); 94 | $actual = $postSettings['searchableAttributes']; 95 | 96 | $this->assertEquals($expected, $actual); 97 | } 98 | 99 | public function testGetSettingsCustomLocation() 100 | { 101 | $org_env = env('ALGOLIA_SETTINGS_FOLDER'); 102 | if ($org_env === null) { 103 | // Currently not set. `putenv('SOME_KEY')`, so without equals sign, 104 | // will actually unset the env variable. 105 | $org_env = 'ALGOLIA_SETTINGS_FOLDER'; 106 | } else { 107 | $org_env = 'ALGOLIA_SETTINGS_FOLDER=' . $org_env; 108 | } 109 | putenv('ALGOLIA_SETTINGS_FOLDER=custom-sub-path-settings/'); 110 | 111 | $sut = new IndexResourceRepository(); 112 | 113 | $expected = [ 114 | 'title', 115 | 'summary', 116 | ]; 117 | 118 | $postSettings = $sut->getSettings((new TestModelWithSearchableTrait)->searchableAs()); 119 | $actual = $postSettings['searchableAttributes']; 120 | 121 | $this->assertEquals($expected, $actual); 122 | 123 | putenv($org_env); 124 | } 125 | 126 | public function testSaveSettings() 127 | { 128 | $settings = [ 129 | 'searchableAttributes' => [ 130 | 'title', 131 | 'summary', 132 | 'tags', 133 | ], 134 | 'replicas' => [ 135 | 'posts_newest', 136 | 'posts_oldest', 137 | ], 138 | 'customRanking' => null, 139 | ]; 140 | 141 | $sut = new IndexResourceRepository(); 142 | 143 | $actual = $sut->saveSettings((new TestModelWithSearchableTrait)->searchableAs(), $settings); 144 | $expected = \json_encode($settings); 145 | 146 | $this->assertNotFalse($actual); 147 | $this->assertJsonStringEqualsJsonString( 148 | $expected, 149 | $this->file_system->getChild('resources/algolia-settings/posts.json')->getContent() 150 | ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /tests/TestModel.php: -------------------------------------------------------------------------------- 1 |