diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d05f945 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 madonetr + +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 100644 index 0000000..f5530cc --- /dev/null +++ b/README.md @@ -0,0 +1,173 @@ +# Laravel Addresses + +An easy way to manage Turkey addresses for Eloquent models in Laravel. +Inspired by the following packages: +- [chuckcms/laravel-addresses](https://github.com/chuckcms/laravel-addresses) +- [rinvex/laravel-addresses](https://github.com/rinvex/laravel-addresses) +- [Lecturize/Laravel-Addresses](https://github.com/Lecturize/Laravel-Addresses) + +## Installation + +Require the package by running + +``` composer require madonetr/laravel-addresses``` + +## Publish configuration and migration +``` php artisan vendor:publish --provider="Madonetr\Addresses\AddressesServiceProvider" ``` + +This command will publish a ```config/addresses.php``` and a migration file. + +> You can modify the default fields and their rules by changing both of these files. + +After publishing you can run the migrations + +``` php artisan migrate ``` + +## Usage + +You can use the ```HasAddresses``` trait on any model. + +```php +addAddress([ + 'label' => 'My address', // required + 'type' => 'Individual', // defaults to: null + 'name' => 'Mustafa Balci', // defaults to: null + 'company' => 'Madonetr', // defaults to: null + 'vat_id' => '12314123', // defaults to: null - integer + 'vat_office' => 'Yalova Vergi Dairesi', // defaults to: null + 'address' => 'Main Street', // defaults to: null + 'postal_code' => '10001', // defaults to: null + 'city' => 'Yalova', // defaults to: null + 'state' => 'New Madonetr State', // defaults to: null + 'country' => 'Turkey', // defaults to: null + 'is_primary' => true, // defaults to: false + 'is_billing' => false, // defaults to: false + 'is_shipping' => false, // defaults to: false +]); +``` + +#### Update an existing address + +```php +$post = Post::first(); +$address = $post->getPrimaryAddress(); + +$post->updateAddress($address, ['label' => 'My new address']); +``` + +#### Delete an address from a model + +```php +$post = Post::first(); +$address = $post->addresses()->first(); + +if ($post->deleteAddress($address)) { + //do something +} +``` + +#### Determine if a model has any addresses + +```php +$post = Post::first(); + +if ($post->hasAddresses()) { + //do something +} +``` + +#### Determine if a model has (one of) the given address(es). + +```php +use Madonetr\Addresses\Models\Address; + +$post = Post::find(); +$address = Address::first(); + +if ($post->hasAddress($address)) { + //do something +} + +//OR +if ($post->hasAddress($address->id)) { + //do something +} + +//OR +$addresses = Address::where('city', 'Yalova')->get(); +if ($post->hasAddress($addresses)) { + //do something +} + +//OR +$addresses = Address::where('state', 'New Madonetr State')->pluck('id')->toArray(); +if ($post->hasAddress($addresses)) { + //do something +} +``` +> This will return true when *one* of the given addresses belongs to the model. + +## Getters + +You can use the following methods to retrieve addresses and certain attributes. + +#### Get the primary address of the model. + +```php +$post = Post::first(); + +$primaryAddress = $post->getPrimaryAddress(); +``` + +#### Get the billing address of the model. + +```php +$post = Post::first(); + +$billingAddress = $post->getBillingAddress(); +``` + +#### Get the shipping address of the model. + +```php +$post = Post::first(); + +$shippingAddress = $post->getShippingAddress(); +``` + +#### Get the labels of all addresses of the model. + +```php +$post = Post::first(); + +$labels = $post->getAddressLabels(); +``` + +## License + +Licensed under [MIT license](http://opensource.org/licenses/MIT). + +## Author + +**Written by [Mustafa Balci](https://twitter.com/mustafabalci__) in Turkey.** diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ada1275 --- /dev/null +++ b/composer.json @@ -0,0 +1,38 @@ +{ + "name": "mstfblci/laravel-addresses", + "description": "Module for storing addresses.", + "keywords": ["laravel", "module", "addresses"], + "homepage": "https://github.com/mstfblci/laravel-addresses", + "license": "MIT", + "authors": [ + { + "name": "Mustafa Balcı", + "email": "madonetr@gmail.com", + "homepage": "", + "role": "Developer" + } + ], + "require": { + "php" : "^7.2.5|^8.0", + "illuminate/auth": "^6.0|^7.0|^8.0", + "illuminate/container": "^6.0|^7.0|^8.0", + "illuminate/contracts": "^6.0|^7.0|^8.0", + "illuminate/database": "^6.0|^7.0|^8.0" + }, + "autoload": { + "psr-4": { + "Madonetr\\Addresses\\": "src" + } + }, + "extra": { + "laravel": { + "providers": [ + "Madonetr\\Addresses\\AddressesServiceProvider" + ], + "aliases": { + + } + } + }, + "minimum-stability": "dev" +} diff --git a/config/addresses.php b/config/addresses.php new file mode 100644 index 0000000..90c8144 --- /dev/null +++ b/config/addresses.php @@ -0,0 +1,53 @@ + [ + + /* + * Which model to use for the Address when using the 'HasAddresses' trait. + * + */ + + 'address' => Madonetr\Addresses\Models\Address::class, + + ], + + 'table_names' => [ + + /* + * Define the table name to use when using the 'HasAddresses' trait. + */ + + 'addresses' => 'addresses', + + ], + + 'column_names' => [ + + 'model_morph_name' => 'addressable', + 'model_morph_key' => 'addressable_id', + 'model_morph_type' => 'addressable_type', + + ], + + 'fields' => [ + 'addresses' => [ + 'label' => 'required|string|max:255', + 'type' => 'nullable|string|max:140', + 'name' => 'nullable|string|max:140', + 'company' => 'nullable|string|max:140', + 'vat_id' => 'nullable|integer', + 'vat_office' => 'nullable|string|max:140', + 'address' => 'nullable|string|max:140', + 'postal_code' => 'nullable|string', + 'city' => 'nullable|string|max:140', + 'state' => 'nullable|string|max:140', + 'country' => 'nullable|string|max:140', + 'is_public' => 'sometimes|boolean', + 'is_primary' => 'sometimes|boolean', + 'is_billing' => 'sometimes|boolean', + 'is_shipping' => 'sometimes|boolean', + ], + ], +]; diff --git a/database/migrations/create_addresses_tables.php.stub b/database/migrations/create_addresses_tables.php.stub new file mode 100644 index 0000000..444cb89 --- /dev/null +++ b/database/migrations/create_addresses_tables.php.stub @@ -0,0 +1,59 @@ +increments('id'); + $table->string('label'); + $table->string('type')->nullable()->default(null); + + $table->string('name')->nullable()->default(null); + $table->string('company')->nullable()->default(null); + + $table->bigInteger('vat_id')->nullable()->default(null); + $table->string('vat_office')->nullable()->default(null); + + $table->string('address')->nullable()->default(null); + $table->string('postal_code')->nullable()->default(null); + $table->string('city')->nullable()->default(null); + $table->string('state')->nullable()->default(null); + $table->string('country')->nullable()->default(null); + + $table->boolean('is_public')->default(false); + $table->boolean('is_primary')->default(false); + $table->boolean('is_billing')->default(false); + $table->boolean('is_shipping')->default(false); + + $table->nullableMorphs($morphName); + + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $tableNames = config('addresses.table_names'); + + Schema::drop($tableNames['addresses']); + } +} diff --git a/src/AddressesServiceProvider.php b/src/AddressesServiceProvider.php new file mode 100644 index 0000000..cd49678 --- /dev/null +++ b/src/AddressesServiceProvider.php @@ -0,0 +1,68 @@ +doPublishing(); + + $this->registerModelBindings(); + } + + public function register() + { + $this->mergeConfigFrom( + __DIR__.'/../config/addresses.php', + 'addresses' + ); + } + + public function doPublishing() + { + if (!function_exists('config_path')) { + // function not available and 'publish' not relevant in Lumen (credit: Spatie) + return; + } + + $this->publishes([ + __DIR__.'/../config/addresses.php' => config_path('addresses.php'), + ], 'config'); + + $this->publishes([ + __DIR__.'/../database/migrations/create_addresses_tables.php.stub' => $this->getMigrationFileName('create_addresses_tables.php'), + ], 'migrations'); + } + + public function registerModelBindings() + { + $config = $this->app->config['addresses.models']; + + $this->app->bind(AddressContract::class, $config['address']); + } + + /** + * Returns existing migration file if found, else uses the current timestamp. + * + * @return string + */ + public function getMigrationFileName($migrationFileName): string + { + $timestamp = date('Y_m_d_His'); + + $filesystem = $this->app->make(Filesystem::class); + + return Collection::make($this->app->databasePath().DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR) + ->flatMap(function ($path) use ($filesystem, $migrationFileName) { + return $filesystem->glob($path.'*_'.$migrationFileName); + }) + ->push($this->app->databasePath()."/migrations/{$timestamp}_{$migrationFileName}") + ->first(); + } +} diff --git a/src/Contracts/Address.php b/src/Contracts/Address.php new file mode 100644 index 0000000..15d74df --- /dev/null +++ b/src/Contracts/Address.php @@ -0,0 +1,17 @@ +setTable(config('addresses.table_names.addresses')); + $this->mergeFillables(); + + parent::__construct($attributes); + } + + /** + * Merge fillable fields. + * + * @return void. + */ + private function mergeFillables() + { + $fillable = $this->fillable; + $columns = array_keys(config('addresses.fields.addresses')); + + $this->fillable(array_merge($fillable, $columns)); + } + + /** + * Get the related model. + * + * @return MorphTo + */ + public function addressable(): MorphTo + { + return $this->morphTo(); + } + + /** + * Get the validation rules. + * + * @return array + */ + public static function getValidationRules(): array + { + $rules = config('addresses.fields.addresses'); + + return $rules; + } + + /** + * Scope public addresses. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeIsPublic(Builder $builder): Builder + { + return $builder->where('is_public', true); + } + + /** + * Scope primary addresses. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeIsPrimary(Builder $builder): Builder + { + return $builder->where('is_primary', true); + } + + /** + * Scope billing addresses. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeIsBilling(Builder $builder): Builder + { + return $builder->where('is_billing', true); + } + + /** + * Scope shipping addresses. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeIsShipping(Builder $builder): Builder + { + return $builder->where('is_shipping', true); + } + + /** + * Scope addresses by the given country. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param string $countryCode + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeInCountry(Builder $builder, string $country): Builder + { + return $builder->where('country', $country); + } + + public static function findById(int $id): AddressContract + { + $address = static::where('id', $id)->first(); + + if (!$address) { + throw AddressDoesNotExist::withId($id); + } + + return $address; + } +} diff --git a/src/Traits/HasAddresses.php b/src/Traits/HasAddresses.php new file mode 100644 index 0000000..9809cc3 --- /dev/null +++ b/src/Traits/HasAddresses.php @@ -0,0 +1,288 @@ +isForceDeleting()) { + $model->addresses()->forceDelete(); + + return; + } + + $model->addresses()->delete(); + }); + } + + public function getAddressClass() + { + if (!isset($this->addressClass)) { + $this->addressClass = config('addresses.models.address'); + } + + return $this->addressClass; + } + + /** + * A model may have multiple addresses. + * + * @return MorphMany + */ + public function addresses(): MorphMany + { + return $this->morphMany( + config('addresses.models.address'), + config('addresses.column_names.model_morph_name'), + config('addresses.column_names.model_morph_type'), + config('addresses.column_names.model_morph_key') + ); + } + + /** + * Check if model has addresses. + * + * @return bool + */ + public function hasAddresses(): bool + { + return (bool) count($this->addresses); + } + + /** + * Add an address to this model. + * + * @param array $attributes + * + * @throws Exception + * + * @return mixed + */ + public function addAddress(array $attributes) + { + $attributes = $this->loadAddressAttributes($attributes); + + return $this->addresses()->create($attributes); + } + + /** + * Updates the given address. + * + * @param Address $address + * @param array $attributes + * + * @throws Exception + * + * @return bool + */ + public function updateAddress(Address $address, array $attributes): bool + { + $attributes = $this->loadAddressAttributes($attributes); + + return $address->fill($attributes)->save(); + } + + /** + * Deletes given address(es). + * + * @param int|array|\Chuck\Address\Contracts\Address $addresses + * @param bool $force + * + * @throws Exception + * + * @return mixed + */ + public function deleteAddress($addresses, $force = false): bool + { + if (is_int($addresses) && $this->hasAddress($addresses)) { + return $force ? + $this->addresses()->where('id', $addresses)->forceDelete() : + $this->addresses()->where('id', $addresses)->delete(); + } + + if ($addresses instanceof Address && $this->hasAddress($addresses)) { + return $force ? + $this->addresses()->where('id', $addresses->id)->forceDelete() : + $this->addresses()->where('id', $addresses->id)->delete(); + } + + if (is_array($addresses)) { + foreach ($addresses as $address) { + if ($this->deleteAddress($address, $force)) { + continue; + } + } + + return true; + } + + return false; + } + + /** + * Forcefully deletes given address(es). + * + * @param int|array|\Chuck\Address\Contracts\Address $addresses + * @param bool $force + * + * @throws Exception + * + * @return mixed + */ + public function forceDeleteAddress($addresses): bool + { + return $this->deleteAddress($addresses, true); + } + + /** + * Determine if the model has (one of) the given address(es). + * + * @param int|array|\Chuck\Address\Contracts\Address|\Illuminate\Support\Collection $addresses + * + * @return bool + */ + public function hasAddress($addresses): bool + { + if (is_int($addresses)) { + return $this->addresses->contains('id', $addresses); + } + + if ($addresses instanceof Address) { + return $this->addresses->contains('id', $addresses->id); + } + + if (is_array($addresses)) { + foreach ($addresses as $address) { + if ($this->hasAddress($address)) { + return true; + } + } + + return false; + } + + return $addresses->intersect($this->addresses)->isNotEmpty(); + } + + public function getAddressLabels(): Collection + { + return $this->addresses->pluck('label'); + } + + /** + * Get the public address. + * + * @param string $direction + * + * @return Address|null + */ + public function getPublicAddress(string $direction = 'desc'): ?Address + { + return $this->addresses() + ->isPublic() + ->orderBy('is_public', $direction) + ->first(); + } + + /** + * Get the primary address. + * + * @param string $direction + * + * @return Address|null + */ + public function getPrimaryAddress(string $direction = 'desc'): ?Address + { + return $this->addresses() + ->isPrimary() + ->orderBy('is_primary', $direction) + ->first(); + } + + /** + * Get the billing address. + * + * @param string $direction + * + * @return Address|null + */ + public function getBillingAddress(string $direction = 'desc'): ?Address + { + return $this->addresses() + ->isBilling() + ->orderBy('is_billing', $direction) + ->first(); + } + + /** + * Get the first shipping address. + * + * @param string $direction + * + * @return Address|null + */ + public function getShippingAddress(string $direction = 'desc'): ?Address + { + return $this->addresses() + ->isShipping() + ->orderBy('is_shipping', $direction) + ->first(); + } + + /** + * Add country id to attributes array. + * + * @param array $attributes + * + * @throws FailedValidation + * + * @return array + */ + public function loadAddressAttributes(array $attributes): array + { + if (!isset($attributes['label'])) { + throw new FailedValidation('[Addresses] No label given.'); + } + + $validator = $this->validateAddress($attributes); + + if ($validator->fails()) { + $errors = $validator->errors()->all(); + $error = '[Addresses] '.implode(' ', $errors); + + throw new FailedValidation($error); + } + + return $attributes; + } + + /** + * Validate the address. + * + * @param array $attributes + * + * @return Validator + */ + public function validateAddress(array $attributes): Validator + { + $rules = (new AddressModel())->getValidationRules(); + + return validator($attributes, $rules); + } +}