diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..a1eb98e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea + +build +vendor +composer.lock +.phpunit.result.cache \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100755 index 0000000..0041a07 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: php + +php: + - 7.1.3 + +before_install: + - travis_retry composer self-update + +install: + - travis_retry composer install --no-interaction --prefer-dist + +script: + - vendor/bin/phpunit --verbose --coverage-clover=coverage.xml + - vendor/bin/phpmd src text phpmd.xml --exclude src/database + +after_success: + - bash <(curl -s https://codecov.io/bash) + +matrix: + fast_finish: true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..000e5af --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 stylers-llc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100755 index 0000000..8235b02 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# Laravel Address + +[![Latest Stable Version](https://poser.pugx.org/stylers/laravel-address/version)](https://packagist.org/packages/stylers/laravel-address) +[![Total Downloads](https://poser.pugx.org/stylers/laravel-address/downloads)](https://packagist.org/packages/stylers/laravel-address) +[![License](https://poser.pugx.org/stylers/laravel-address/license)](https://packagist.org/packages/stylers/laravel-address) +[![Build Status](https://travis-ci.org/stylers-llc/laravel-address.svg?branch=master)](https://travis-ci.org/stylers-llc/laravel-address) +[![codecov](https://codecov.io/gh/stylers-llc/laravel-address/branch/master/graph/badge.svg)](https://codecov.io/gh/stylers-llc/laravel-address) + +## Requirements +- PHP >= 7.1.3 +- Laravel ~5.x + +## Installation +```bash +composer require stylers/laravel-address +``` + +You can publish the migration +```bash +php artisan vendor:publish --provider="Stylers\Address\Providers\AddressServiceProvider" +``` + +After the migration has been published, you can run the migrations +```bash +php artisan migrate +``` + +## Usage +* How to address +```php +use Stylers\Address\Contracts\Models\Traits\HasAddressesInterface; +use Stylers\Address\Models\Traits\HasAddresses; + +class User extends Authenticatable implements HasAddressesInterface +{ + use Notifiable; + use HasAddresses; +} +``` + +## Update or Create Address +```php +use Stylers\Address\Enums\AddressTypeEnum; + +$user = User::first(); +$attributes = [ + "country" => "Hungary", + "zip_code" => "1055", + "city" => "Budapest", + "name_of_public_place" => "Kossuth Lajos", + "type_of_public_place" => "place", + "number_of_house" => "1-3", +]; // array +$type = AddressTypeEnum::PRIMARY; // ?string +$address = $user->updateOrCreateAddress($attributes, $type); // AddressInterface +``` + +## Delete Address +```php +use Stylers\Address\Enums\AddressTypeEnum; + +$user = User::first(); +$type = AddressTypeEnum::PRIMARY; // ?string +$isDeleted = $user->deleteAddress($type); // boolean +``` + +## Sync Address(es) +The `syncAddresses()` method delete all addressable address if $`type` is not exists in `$arrayOfAttributes[$type][]`. +The `syncAddresses()` method create all `$type` of `arrayOfAttributes[$type][]` if type is not exists in `addresses` table. +```php +use Stylers\Address\Enums\AddressTypeEnum; + +$user = User::first(); +$arrayOfAttributes = [ + AddressTypeEnum::MAILING => [ + "country" => "Hungary", + "zip_code" => "1055", + "city" => "Budapest", + "name_of_public_place" => "Kossuth Lajos", + "type_of_public_place" => "place", + "number_of_house" => "1-3", + ] +]; +$addresses = $user->syncAddresses($arrayOfAttributes); // Collection +``` \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100755 index 0000000..479a5bc --- /dev/null +++ b/composer.json @@ -0,0 +1,61 @@ +{ + "name": "stylers/laravel-address", + "description": "Laravel Address Manager", + "homepage": "https://github.com/stylers-llc/laravel-address", + "keywords": [ + "stylers", + "laravel", + "address", + "addressable" + ], + "type": "library", + "license": "MIT", + "minimum-stability": "dev", + "prefer-stable": true, + "authors": [ + { + "name": "Szilveszter Nagy", + "email": "developer@stylersonline.com", + "homepage": "http://stylers.hu", + "role": "Developer" + } + ], + "require": { + "php": ">=7.1.3", + "illuminate/database": "~5", + "illuminate/support": "~5" + }, + "require-dev": { + "mockery/mockery": "^1.1", + "orchestra/testbench": "~3.0|~3.6|~3.7", + "phpmd/phpmd": "^2.6", + "phpunit/phpunit": "^7.3" + }, + "autoload": { + "classmap": [ + "database" + ], + "psr-4": { + "Stylers\\Address\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Stylers\\Address\\Tests\\": "tests", + "Stylers\\Address\\Tests\\Fixtures\\": "tests/fixtures" + } + }, + "scripts": { + "test": "vendor/bin/phpunit" + }, + "config": { + "sort-packages": true + }, + "extra": { + "laravel": { + "providers": [ + "Stylers\\Address\\Providers\\AddressServiceProvider" + ] + } + } +} diff --git a/database/migrations/2018_07_09_104935_create_addresses_table.php b/database/migrations/2018_07_09_104935_create_addresses_table.php new file mode 100755 index 0000000..0f2100e --- /dev/null +++ b/database/migrations/2018_07_09_104935_create_addresses_table.php @@ -0,0 +1,39 @@ +increments('id'); + $table->morphs('addressable'); + $table->string('country')->nullable(); + $table->string('zip_code')->nullable(); + $table->string('city')->nullable(); + $table->string('name_of_public_place')->nullable(); // example: "Kossuth Lajos" + $table->string('type_of_public_place')->nullable(); // example: "street" "place" + $table->string('number_of_house')->nullable(); // example: "1-3" + $table->string('type')->nullable(); // AddressTypeEnum + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('addresses'); + } +} diff --git a/phpmd.xml b/phpmd.xml new file mode 100755 index 0000000..4826a50 --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,45 @@ + + + Package Code Check + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100755 index 0000000..7bd5975 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,32 @@ + + + + + tests/Unit + + + + + src + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Contracts/Enums/EnumInterface.php b/src/Contracts/Enums/EnumInterface.php new file mode 100755 index 0000000..b781a50 --- /dev/null +++ b/src/Contracts/Enums/EnumInterface.php @@ -0,0 +1,15 @@ +getConstants(); + + return array_sort($constants); + } +} diff --git a/src/Enums/AddressTypeEnum.php b/src/Enums/AddressTypeEnum.php new file mode 100644 index 0000000..e15ebf3 --- /dev/null +++ b/src/Enums/AddressTypeEnum.php @@ -0,0 +1,14 @@ +morphTo(); + } +} diff --git a/src/Models/Traits/HasAddresses.php b/src/Models/Traits/HasAddresses.php new file mode 100644 index 0000000..d0f3ea8 --- /dev/null +++ b/src/Models/Traits/HasAddresses.php @@ -0,0 +1,72 @@ +morphMany(app(AddressInterface::class), 'addressable'); + } + + /** + * @param string|null $type + * @return bool + */ + public function hasAddress(string $type = null): bool + { + return (bool)$this->addresses()->where('type', $type)->first(); + } + + + /** + * @param array $attributes + * @param string|null $type + * @return AddressInterface|Model + */ + public function updateOrCreateAddress(array $attributes, string $type = null): AddressInterface + { + $attributes['type'] = $type; + return $this->addresses()->updateOrCreate(['type' => $type], $attributes); + } + + + /** + * @param string|null $type + * @return bool + * @throws \Exception|ModelNotFoundException + */ + public function deleteAddress(string $type = null): bool + { + return $this->addresses()->where('type', $type)->firstOrFail()->delete(); + } + + /** + * @param array $arrayOfAttributes + * @return Collection + */ + public function syncAddresses(array $arrayOfAttributes): Collection + { + $collection = new Collection(); + foreach ($arrayOfAttributes as $type => $attributes) { + $address = $this->updateOrCreateAddress($attributes, $type); + $collection->push($address); + } + + $ids = $collection->pluck('id')->toArray(); + $this->addresses()->whereNotIn('id', $ids)->each(function ($address) { + $address->delete(); + }); + + return $collection; + } +} \ No newline at end of file diff --git a/src/Providers/AddressServiceProvider.php b/src/Providers/AddressServiceProvider.php new file mode 100644 index 0000000..9ad13c3 --- /dev/null +++ b/src/Providers/AddressServiceProvider.php @@ -0,0 +1,36 @@ +publishes([ + __DIR__ . '/../../database/migrations' => database_path('migrations'), + ], 'migrations'); + } + + /** + * Register any application services. + * + * @return void + */ + public function register() + { + $this->app->bind(AddressInterface::class, Address::class); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100755 index 0000000..e0eac4a --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,107 @@ +setUpFactory(); + $this->setUpDatabase(); + } + + /** + * Teardown + */ + public function tearDown() + { + $this->consoleOutput = ''; + $this->artisan('migrate:reset'); + + parent::tearDown(); + } + + /** + * @param \Illuminate\Foundation\Application $app + * @return array + */ + protected function getPackageProviders($app) + { + return [ + AddressServiceProvider::class, +// \Orchestra\Database\ConsoleServiceProvider::class, + ]; + } + + /** + * Resolve application Console Kernel implementation. + * + * @param \Illuminate\Foundation\Application $app + */ + public function resolveApplicationConsoleKernel($app) + { + $app->singleton(Kernel::class, \Orchestra\Testbench\Console\Kernel::class); + } + + /** + * Define environment setup + * + * @param \Illuminate\Foundation\Application $app + */ + protected function getEnvironmentSetUp($app) + { + $app['config']->set('database.default', 'testing'); + $app['config']->set('database.connections.testing', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + ]); + } + + /** + * Configure the factory + */ + private function setUpFactory() + { + $this->withFactories(__DIR__ . '/../database/factories'); + $this->withFactories(__DIR__ . '/fixtures/database/factories'); + } + + /** + * Configure the database + * SQLite + */ + private function setUpDatabase() + { + $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); + + $this->artisan('migrate', ['--database' => 'testing']); + $this->artisan('migrate', [ + '--database' => 'testing', +// '--realpath' => realpath(__DIR__ . '/fixtures/database/migrations'), + '--path' => '../../../../tests/fixtures/database/migrations', + ]); + + } +} \ No newline at end of file diff --git a/tests/Unit/Models/AddressTest.php b/tests/Unit/Models/AddressTest.php new file mode 100755 index 0000000..303abce --- /dev/null +++ b/tests/Unit/Models/AddressTest.php @@ -0,0 +1,33 @@ +create(); + $address = factory(Address::class)->make(); + $address->addressable()->associate($addressable); + $address->save(); + $address->refresh(); + + $this->assertEquals($class, get_class($address->addressable)); + } +} \ No newline at end of file diff --git a/tests/Unit/Models/Traits/HasAddressesTest.php b/tests/Unit/Models/Traits/HasAddressesTest.php new file mode 100755 index 0000000..8ab3f7d --- /dev/null +++ b/tests/Unit/Models/Traits/HasAddressesTest.php @@ -0,0 +1,252 @@ +make(); + $addressable = factory($class)->create(); + $addressable->addresses()->save($address); + $addressable->refresh(); + $attachedAddress = $addressable->addresses()->first(); + + $this->assertInstanceOf(Address::class, $attachedAddress); + } + + /** + * @test + */ + public function it_can_create() + { + $addressable = factory(User::class)->create(); + $attributes = factory(Address::class)->make()->toArray(); + $address = $addressable->updateOrCreateAddress($attributes, $attributes['type']); + + $this->assertDatabaseHas('addresses', $attributes); + $this->assertEquals($addressable->id, $address->addressable_id); + $this->assertEquals(get_class($addressable), $address->addressable_type); + } + + /** + * @test + */ + public function it_can_create_when_type_is_null() + { + $addressable = factory(User::class)->create(); + $attributes = factory(Address::class)->make(['type' => null])->toArray(); + $address = $addressable->updateOrCreateAddress($attributes); + + $this->assertDatabaseHas('addresses', $attributes); + $this->assertEquals($addressable->id, $address->addressable_id); + $this->assertEquals(get_class($addressable), $address->addressable_type); + } + + /** + * @test + */ + public function it_can_not_override_type_from_attributes() + { + $addressable = factory(User::class)->create(); + $attributes = factory(Address::class)->make(['type' => AddressTypeEnum::PRIMARY])->toArray(); + $type = AddressTypeEnum::BILLING; + $address = $addressable->updateOrCreateAddress($attributes, $type); + + $this->assertEquals($type, $address->type); + + $attributes['type'] = $type; + $this->assertDatabaseHas('addresses', $attributes); + $this->assertEquals($addressable->id, $address->addressable_id); + $this->assertEquals(get_class($addressable), $address->addressable_type); + } + + /** + * @test + */ + public function it_can_update() + { + $addressable = factory(User::class)->create(); + + $attributes = factory(Address::class)->make()->toArray(); + $address = $addressable->updateOrCreateAddress($attributes, $attributes['type']); + + $attributesUpdate = factory(Address::class)->make()->toArray(); + $addressUpdated = $addressable->updateOrCreateAddress($attributesUpdate, $attributes['type']); + + $attributesUpdate['type'] = $attributes['type']; + $this->assertDatabaseHas('addresses', $attributesUpdate); + $this->assertEquals($address->id, $addressUpdated->id); + $this->assertEquals($addressable->id, $addressUpdated->addressable_id); + $this->assertEquals(get_class($addressable), $addressUpdated->addressable_type); + } + + /** + * @test + */ + public function has_address() + { + $addressable = factory(User::class)->create(); + $attributes = factory(Address::class)->make()->toArray(); + $addressable->updateOrCreateAddress($attributes, $attributes['type']); + + $this->assertTrue($addressable->hasAddress($attributes['type'])); + } + + /** + * @test + */ + public function has_address_when_type_is_null() + { + $addressable = factory(User::class)->create(); + $attributes = factory(Address::class)->make(['type' => null])->toArray(); + $addressable->updateOrCreateAddress($attributes); + + $this->assertTrue($addressable->hasAddress()); + } + + /** + * @test + */ + public function has_not_address() + { + $addressable = factory(User::class)->create(); + $attributes = factory(Address::class)->make(['type' => AddressTypeEnum::BILLING])->toArray(); + $addressable->updateOrCreateAddress($attributes, $attributes['type']); + + $addressableOther = factory(User::class)->create(); + $attributesOther = factory(Address::class)->make(['type' => AddressTypeEnum::PRIMARY])->toArray(); + $addressableOther->updateOrCreateAddress($attributes, $attributesOther['type']); + + $this->assertFalse($addressable->hasAddress($attributesOther['type'])); + } + + /** + * @test + */ + public function it_can_delete() + { + $addressable = factory(User::class)->create(); + $attributes = factory(Address::class)->make()->toArray(); + $addressable->updateOrCreateAddress($attributes, $attributes['type']); + + $response = $addressable->deleteAddress($attributes['type']); + + $this->assertDatabaseMissing('addresses', $attributes); + $this->assertTrue($response); + } + + /** + * @test + */ + public function it_can_delete_when_type_is_null() + { + $addressable = factory(User::class)->create(); + $attributes = factory(Address::class)->make(['type' => null])->toArray(); + $addressable->updateOrCreateAddress($attributes); + + $response = $addressable->deleteAddress(); + + $this->assertDatabaseMissing('addresses', $attributes); + $this->assertTrue($response); + } + + /** + * @test + * @expectedException \Illuminate\Database\Eloquent\ModelNotFoundException + */ + public function it_can_not_delete() + { + $addressable = factory(User::class)->create(); + $attributes = factory(Address::class)->make(['type' => AddressTypeEnum::BILLING])->toArray(); + $addressable->updateOrCreateAddress($attributes, $attributes['type']); + + $addressable->deleteAddress(AddressTypeEnum::PRIMARY); + } + + /** + * @test + */ + public function it_can_sync() + { + $addressable = factory(User::class)->create(); + $attributes = factory(Address::class)->make()->toArray(); + $type = AddressTypeEnum::BILLING; + $address = $addressable->updateOrCreateAddress($attributes, $type); + + $attributesOfSync = [ + $type => factory(Address::class)->make(['type' => $type])->toArray() + ]; + + $addresses = $addressable->syncAddresses($attributesOfSync); + + $this->assertDatabaseHas('addresses', $attributesOfSync[$type]); + $this->assertEquals(1, $addressable->addresses()->get()->count()); + $this->assertEquals(1, $addresses->count()); + $this->assertEquals($address->id, $addresses->first()->id); + $this->assertEquals($addressable->id, $addresses->first()->addressable_id); + $this->assertEquals(get_class($addressable), $addresses->first()->addressable_type); + } + + /** + * @test + */ + public function it_can_sync_with_delete() + { + $addressable = factory(User::class)->create(); + + $types = AddressTypeEnum::getConstants(); + foreach ($types as $type) { + $attributes[$type] = factory(Address::class)->make(['type' => $type])->toArray(); + $addressable->updateOrCreateAddress($attributes, $type); + } + + $typeSynced = AddressTypeEnum::BILLING; + $attributesOfSync = [ + $typeSynced => factory(Address::class)->make(['type' => $typeSynced])->toArray() + ]; + + $addressable->syncAddresses($attributesOfSync); + foreach ($types as $type) { + $this->assertEquals($type == $typeSynced, $addressable->hasAddress($type)); + } + } + + /** + * @test + */ + public function it_can_sync_with_create() + { + $addressable = factory(User::class)->create(); + $attributes = factory(Address::class)->make(['type' => AddressTypeEnum::BILLING])->toArray(); + $addressable->updateOrCreateAddress($attributes, $attributes['type']); + + $attributesOfSync = [ + $attributes['type'] => factory(Address::class)->make()->toArray(), + AddressTypeEnum::PRIMARY => factory(Address::class)->make()->toArray(), + ]; + + $addressable->syncAddresses($attributesOfSync); + $types = AddressTypeEnum::getConstants(); + foreach ($types as $type) { + $this->assertEquals(array_key_exists($type, $attributesOfSync), $addressable->hasAddress($type)); + } + } +} \ No newline at end of file diff --git a/tests/fixtures/Models/User.php b/tests/fixtures/Models/User.php new file mode 100755 index 0000000..0575d8b --- /dev/null +++ b/tests/fixtures/Models/User.php @@ -0,0 +1,16 @@ +define(Address::class, function (Faker $faker) { + return [ + 'type' => array_random(AddressTypeEnum::getConstants()), + 'country' => $faker->country, + 'zip_code' => $faker->postcode, + 'city' => $faker->city, + 'name_of_public_place' => $faker->streetName, + 'type_of_public_place' => 'street', + 'number_of_house' => $faker->randomNumber(), + ]; +}); diff --git a/tests/fixtures/database/factories/UserFactory.php b/tests/fixtures/database/factories/UserFactory.php new file mode 100755 index 0000000..c26a09f --- /dev/null +++ b/tests/fixtures/database/factories/UserFactory.php @@ -0,0 +1,23 @@ +define(User::class, function (Faker $faker) { + return [ + 'name' => $faker->name(), + 'email' => $faker->email + ]; +}); \ No newline at end of file diff --git a/tests/fixtures/database/migrations/2018_07_11_154227_create_users_table.php b/tests/fixtures/database/migrations/2018_07_11_154227_create_users_table.php new file mode 100755 index 0000000..997ef46 --- /dev/null +++ b/tests/fixtures/database/migrations/2018_07_11_154227_create_users_table.php @@ -0,0 +1,35 @@ +increments('id'); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamps(); + }); + } + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('users'); + } +} \ No newline at end of file