This commit is contained in:
2025-09-25 03:03:31 +05:00
commit ae480cf2f6
2768 changed files with 1485826 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models\Ecommerce\Product\Product\Concerns;
use Laravel\Nova\Nova;
trait ProductFrontEndHelpers
{
/**
* Show page
*/
public function showPage(): string
{
return route('web.products.show', ['product' => $this->slug]);
}
/**
* Nova page
*/
public function novaPage(): string
{
return Nova::url('/resources/products');
}
/**
* Nova detail page
*/
public function novaDetailPage(): string
{
return Nova::url('/resources/products/'.$this->id);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Models\Ecommerce\Product\Product\Concerns;
use Spatie\Image\Manipulations;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
trait ProductMedia
{
/**
* Media collections
*/
public function registerMediaCollections(): void
{
$this->addMediaCollection('uploads')
->useFallbackUrl(url('/assets/web/images/05.jpg'));
}
/**
* Media Conversions
*/
public function registerMediaConversions(?Media $media = null): void
{
$this->addMediaConversion('thumb200x200')
->fit(Manipulations::FIT_CONTAIN, 200, 200);
$this->addMediaConversion('thumb400x400')
->fit(Manipulations::FIT_CONTAIN, 400, 400);
$this->addMediaConversion('thumb720x720')
->fit(Manipulations::FIT_CONTAIN, 720, 720);
$this->addMediaConversion('thumb800x800')
->fit(Manipulations::FIT_CONTAIN, 800, 800);
$this->addMediaConversion('thumb1200x1200')
->fit(Manipulations::FIT_CONTAIN, 1200, 1200);
$this->addMediaConversion('thumb288x431')
->fit(Manipulations::FIT_CONTAIN, 288, 431);
$this->addMediaConversion('thumb270x350')
->fit(Manipulations::FIT_CONTAIN, 270, 350);
}
/**
* Thumbnail
*/
public function thumbnail(string $size = '200x200'): string
{
return $this->getFirstMediaUrl('uploads', 'thumb'.$size);
}
/**
* Get image when hovered (returns second image)
*/
public function getHoverImage(string $size = '270x350'): string
{
$media = $this->getMedia('uploads');
$image_count = $media->count();
if ($image_count == 0) {
return '';
}
return ($image_count == 1)
? $media[0]->getUrl('thumb'.$size)
: $media[1]->getUrl('thumb'.$size);
}
}

View File

@@ -0,0 +1,436 @@
<?php
namespace App\Models\Ecommerce\Product\Product\Concerns;
use App\Models\Ecommerce\Product\Property\ProductProperty;
use Illuminate\Support\Facades\Blade;
trait ProductProperties
{
/**
* Check if given product has given property
*/
public function containsProperty(string $property): bool
{
return $this->properties->pluck('attribute.slug')->contains($property);
}
/**
* Get product property
*/
public function getProperty(string $name): ?ProductProperty
{
return $this->properties->first(fn ($property) => $property->attribute->slug === $name);
}
/**
* Check if product has color variants
*/
public function hasColorVariants(): bool
{
return $this->containsProperty('colour');
}
/**
* Get product color property
*/
public function getColorProperty(): ?ProductProperty
{
return $this->getProperty('colour');
}
/**
* Get product color property
*/
public function getSizeProperty(): ?ProductProperty
{
return $this->getProperty('size');
}
/**
* Get color value of product
*/
public function getColorValue(): ?string
{
$colorProperty = $this->getColorProperty();
return $colorProperty ? $colorProperty->values->first()?->real_value : null;
}
/**
* Get size value
*/
public function getSizeValue(): ?string
{
$sizeProperty = $this->getSizeProperty();
return $sizeProperty ? $sizeProperty->values->first()?->real_value : null;
}
/**
* Check if product has a color.
*/
public function hasColor(): bool
{
return $this->hasPropertyWithSlug('colour');
}
/**
* Check if product has a size.
*/
public function hasSize(): bool
{
return $this->hasPropertyWithSlug('size');
}
/**
* Color property
*/
public function getColor(): string
{
$product = 'productColor'.$this->id;
if (temp_cache($product)) {
return temp_cache($product);
}
cache()->put(
key: 'productColor'.$this->id,
value: $this->getPropertyValueBySlug('colour'),
);
return temp_cache($product);
}
/**
* Size property
*/
public function getSize(): string
{
$product = 'productSize'.$this->id;
if (temp_cache($product)) {
return temp_cache($product);
}
cache()->put(
'productSize'.$this->id,
$this->getPropertyValueBySlug('size'),
);
return temp_cache($product);
}
/**
* Color variants
*/
public function colorVariants(): array
{
if (temp_cache('productColorVariants')) {
return temp_cache('productColorVariants');
}
$this->loadMissing(['media', 'properties', 'variations' => ['media', 'properties']]);
$colorVariants = collect();
// Add parent product
$colorVariants->push([
'slug' => $this->slug,
'id' => $this->id,
'image' => $this->thumbnail(),
'color' => $this->getColor(),
]);
$this->variations->each(function ($variation) use ($colorVariants) {
$colorVariants->push([
'slug' => $variation->slug,
'id' => $variation->id,
'image' => $variation->thumbnail(),
'color' => $variation->getColor(),
]);
});
cache()->put(
'productColorVariants',
$colorVariants->uniqueStrict('color')->toArray(),
);
return $colorVariants->toArray();
}
/**
* Check if a property with the given attribute slug exists
*/
protected function hasPropertyWithSlug(string $slug): bool
{
if ($this->relationLoaded('properties')) {
return $this->checkLoadedPropertiesForSlug($slug);
}
return $this->checkDatabaseForPropertySlug($slug);
}
/**
* Check loaded properties for attribute slug
*/
protected function checkLoadedPropertiesForSlug(string $slug): bool
{
foreach ($this->properties as $property) {
if ($property->attribute->slug === $slug) {
return true;
}
}
return false;
}
/**
* Check database for attribute slug
*/
protected function checkDatabaseForPropertySlug(string $slug): bool
{
return $this->properties()
->where('attribute.slug', $slug)
->exists();
}
/**
* Get property value by slug
*/
public function getPropertyValueBySlug(string $slug): string
{
return $this->properties->firstWhere('attribute.slug', 'colour')->values->first()->real_value ?? '';
}
/**
* Get size variants available for given color
*/
public function getSizeVariantsForColor(): array
{
if (temp_cache('productSizeVariantsForColor')) {
return temp_cache('productSizeVariantsForColor');
}
if ($this->parent_id) {
return $this->childSizeVariantsForColor();
}
$this->loadMissing(['properties', 'variations', 'variations.properties']);
$properties = $this->properties;
$variations = $this->variations;
$productSize = [];
$matchingProducts = [];
foreach ($properties as $property) {
if ($property->attribute->slug === 'size') {
$productSize = $property->values->first()->real_value;
$productSize = [
'slug' => $this->slug,
'id' => $this->id,
'size' => $productSize,
'parent_id' => null,
];
}
if ($property->attribute->slug === 'colour') {
// Color found
$productColor = $property->values->first()->real_value;
// Get All Sizes
$variationAllSizes = [];
// Iterate over to find same colors in variants
foreach ($variations as $variation) {
foreach ($variation->properties as $variationProperty) {
if ($variationProperty->attribute->slug === 'size') {
$productSize = $variationProperty->values->first()->real_value;
$variationAllSizes[] = [
'slug' => $variation->slug,
'id' => $variation->id,
'parent_id' => $variation->parent_id,
'size' => $productSize,
];
}
if ($variationProperty->attribute->slug === 'colour') {
// If colors match, append to array
$variationColor = $variationProperty->values->first()->real_value;
if ($productColor === $variationColor) {
$matchingProducts[] = [
'slug' => $variation->slug,
'id' => $variation->id,
'color' => $productColor,
];
}
}
}
}
} /* Color end */
}
$result = collect();
$result->push($productSize);
collect($variationAllSizes)->whereIn('id', collect($matchingProducts)->pluck('id'))
->each(function ($variationSize) use ($result) {
$result->push($variationSize);
});
cache()->put(
'productSizeVariantsForColor',
$result->toArray(),
);
return $result->toArray();
}
public function childSizeVariantsForColor(): array
{
$this->loadMissing(['properties']);
$properties = $this->properties;
$parent = $this->parent;
$parent->load(['properties', 'variations', 'variations.properties']);
$variations = $parent->variations;
$productSize = [];
$matchingProducts = [];
foreach ($properties as $property) {
if ($property->attribute->slug === 'size') {
$productSize = $property->values->first()->real_value;
$productSize = [
'slug' => $this->slug,
'id' => $this->id,
'size' => $productSize,
'parent_id' => $this->parent_id,
];
}
if ($property->attribute->slug === 'colour') {
// Color found
$productColor = $property->values->first()->real_value;
// Get All Sizes
$variationAllSizes = [];
// Iterate over to find same colors in variants
foreach ($variations as $variation) {
if ($variation->id === $this->id) {
continue;
}
foreach ($variation->properties as $variationProperty) {
if ($variationProperty->attribute->slug === 'size') {
$productSize = $variationProperty->values->first()->real_value;
$variationAllSizes[] = [
'slug' => $variation->slug,
'id' => $variation->id,
'parent_id' => $variation->parent_id,
'size' => $productSize,
];
}
if ($variationProperty->attribute->slug === 'colour') {
// If colors match, append to array
$variationColor = $variationProperty->values->first()->real_value;
if ($productColor === $variationColor) {
$matchingProducts[] = [
'slug' => $variation->slug,
'id' => $variation->id,
'color' => $productColor,
];
}
}
}
}
} /* Color end */
}
$result = collect();
$result->push($productSize);
collect($variationAllSizes)->whereIn('id', collect($matchingProducts)->pluck('id'))
->each(function ($variationSize) use ($result) {
$result->push($variationSize);
});
return $result->toArray();
}
/**
* All values of product properties
*/
public function allValuesOfProperties(): array
{
return $this->properties->flatMap(function ($property) {
return $property->values->map(function ($value) {
return $value->real_value;
})->filter();
})->all();
}
/**
* Name with properties
*/
public function nameWithProperties(): string
{
$properties = $this->allValuesOfProperties();
return sprintf(
'%s %s %s',
$this->name,
count($properties) > 0 ? '-' : '',
Blade::render('
@foreach($datas as $data)
<strong>{{ $data }}{{ $loop->last ? "" : "," }}</strong>
@endforeach
', ['datas' => $properties])
);
}
public function getSizeVariants(): array
{
$sizeVariants = [];
// Add parent product
$sizeVariants[] = [
'slug' => $this->slug,
'id' => $this->id,
'image' => $this->thumbnail(),
'images_hd' => $this->getMedia('uploads')->map(fn ($media) => $media->getUrl('thumb720x720')),
'size' => $this->getSizeValue(),
];
$this->variations->each(function ($variation) use (&$sizeVariants) {
$variationSize = $variation->getSizeValue();
if ($variationSize && ! keyValueExistsInArray(datas: $sizeVariants, key: 'size', value: $variationSize)) {
$sizeVariants[] = [
'slug' => $variation->slug,
'id' => $variation->id,
'image' => $variation->thumbnail(),
'images_hd' => $variation->getMedia('uploads')->map(fn ($media) => $media->getUrl('thumb720x720')),
'size' => $variationSize,
];
}
});
return $sizeVariants;
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace App\Models\Ecommerce\Product\Product\Concerns;
use App\Models\Common\Comment;
use App\Models\Ecommerce\Channel\Channel;
use App\Models\Ecommerce\Product\Brand\Brand;
use App\Models\Ecommerce\Product\Cart\Cart;
use App\Models\Ecommerce\Product\Category\Category;
use App\Models\Ecommerce\Product\Collection\Collection;
use App\Models\Ecommerce\Product\Inventory\Inventory;
use App\Models\Ecommerce\Product\Product\Product;
use App\Models\Ecommerce\Product\Property\ProductProperty;
use App\Models\Ecommerce\Product\Review\Review;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
trait ProductRelationships
{
/**
* Product variations
*/
public function variations(): HasMany
{
return $this->hasMany(Product::class, 'parent_id');
}
/**
+ * Parent
+ */
public function parent(): BelongsTo
{
return $this->belongsTo(Product::class, 'parent_id');
}
/**
* Related Channels
*/
public function channels(): MorphToMany
{
return $this->morphedByMany(Channel::class, 'productable', 'product_has_relations');
}
/**
* Firt channel
*/
public function owner(): ?Channel
{
return $this->relationLoaded('channels')
? $this->channels->first()
: $this->channels()->first();
}
/**
* Related products (similar)
*/
public function relatedProducts(): MorphToMany
{
return $this->morphedByMany(Product::class, 'productable', 'product_has_relations');
}
/**
* Related categories
*/
public function categories(): MorphToMany
{
return $this->morphedByMany(Category::class, 'productable', 'product_has_relations');
}
/**
* Related Collections
*/
public function collections(): MorphToMany
{
return $this->morphedByMany(Collection::class, 'productable', 'product_has_relations');
}
/**
* Related Brand
*/
public function brand(): BelongsTo
{
return $this->belongsTo(Brand::class, 'brand_id');
}
/**
* Product Properties, same as attribtues
*
* (size, color, material, ..)
*/
public function properties(): HasMany
{
return $this->hasMany(ProductProperty::class);
}
/**
* Related inventories
*/
public function inventories(): BelongsToMany
{
return $this->belongsToMany(Inventory::class)
->withPivot('stock');
}
/**
* Carts
*/
public function carts(): BelongsTo
{
return $this->belongsTo(Cart::class);
}
/**
* Product's review
*/
public function reviews(): HasMany
{
return $this->hasMany(Review::class);
}
/**
* Product's comments
*/
public function comments(): BelongsToMany
{
return $this->belongsToMany(Comment::class);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Models\Ecommerce\Product\Product\Concerns;
trait ProductScopeQueries
{
/**
* Visable products scope
*
* @param mixed $query
*/
public function scopeVisable($query): void
{
$query->where('is_visible', true);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Models\Ecommerce\Product\Product\Concerns;
trait ProductStates
{
/**
* Check if product is new (one week range)
*/
public function isNew(): bool
{
return $this->created_at->greaterThan(now()->subWeek());
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Models\Ecommerce\Product\Product\Concerns;
use Illuminate\Database\Eloquent\Relations\MorphMany;
trait ProductStock
{
/**
* Inventory history
*/
public function inventoryHistories(): MorphMany
{
return $this->morphMany(InventoryHistory::class, 'stockable')->orderBy('created_at', 'desc');
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace App\Models\Ecommerce\Product\Product\Concerns;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Spatie\Sluggable\SlugOptions;
trait ProductTransformers
{
/**
* Get the options for generating the slug.
*/
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug');
}
/**
* Get discount difference
*
* @return int|float
*/
public function discountDifference(): Attribute
{
return Attribute::make(get: fn () => $this->caluculateDiscountDifference());
}
/**
* Discount percentage
*/
public function discountPercentage()
{
return number_format(($this->caluculateDiscountDifference() * 100) / $this->old_price_amount);
}
/**
* Caluculate Discount Difference
*/
public function caluculateDiscountDifference(int|float|null $price_amount = null): float
{
return $this->old_price_amount ? $this->old_price_amount - ($price_amount ?: $this->price_amount) : 0;
}
/**
* Full name
*/
public function fullName(): Attribute
{
return Attribute::make(
get: function () {
$brand = $this->brand ? "({$this->brand->name})" : '';
return $this->name.$brand.$this->colour.$this->size;
}
);
}
/**
* @param bool backorder
*/
public function backOrder(): Attribute
{
return Attribute::make(
get: fn () => $this->options->get('back_order')
);
}
/**
* @param string weight_value
*/
public function weightValue(): Attribute
{
return Attribute::make(
get: fn () => $this->options->get('weight_value')
);
}
/**
* @param string weight_unit
*/
public function weightUnit(): Attribute
{
return Attribute::make(
get: fn () => $this->options->get('weight_unit')
);
}
/**
* @param string height_value
*/
public function heightValue(): Attribute
{
return Attribute::make(
get: fn () => $this->options->get('height_value')
);
}
/**
* @param string height_unit
*/
public function heightUnit(): Attribute
{
return Attribute::make(
get: fn () => $this->options->get('height_unit')
);
}
/**
* @param string width_value
*/
public function widthValue(): Attribute
{
return Attribute::make(
get: fn () => $this->options->get('width_value')
);
}
/**
* @param string width_unit
*/
public function widthUnit(): Attribute
{
return Attribute::make(
get: fn () => $this->options->get('width_unit')
);
}
/**
* @param string depth_value
*/
public function depthValue(): Attribute
{
return Attribute::make(
get: fn () => $this->options->get('depth_value')
);
}
/**
* @param string depth_unit
*/
public function depthUnit(): Attribute
{
return Attribute::make(
get: fn () => $this->options->get('depth_unit')
);
}
/**
* @param string volume_value
*/
public function volumeValue(): Attribute
{
return Attribute::make(
get: fn () => $this->options->get('volume_value')
);
}
/**
* @param string volume_unit
*/
public function volumeUnit(): Attribute
{
return Attribute::make(
get: fn () => $this->options->get('volume_unit')
);
}
/**
* Flexible Content
*/
// public function flexibleContent(): Attribute
// {
// return Attribute::make(
// get: fn () => $this->flexible('flexible-content')
// );
// }
}

View File

@@ -0,0 +1,121 @@
<?php
namespace App\Models\Ecommerce\Product\Product;
use App\Models\Concerns\HasSchemalessAttributes;
use App\Models\Ecommerce\Product\Product\Concerns\ProductFrontEndHelpers;
use App\Models\Ecommerce\Product\Product\Concerns\ProductMedia;
use App\Models\Ecommerce\Product\Product\Concerns\ProductProperties;
use App\Models\Ecommerce\Product\Product\Concerns\ProductRelationships;
use App\Models\Ecommerce\Product\Product\Concerns\ProductScopeQueries;
use App\Models\Ecommerce\Product\Product\Concerns\ProductStates;
use App\Models\Ecommerce\Product\Product\Concerns\ProductStock;
use App\Models\Ecommerce\Product\Product\Concerns\ProductTransformers;
use CyrildeWit\EloquentViewable\Contracts\Viewable;
use CyrildeWit\EloquentViewable\InteractsWithViews;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\Sluggable\HasSlug;
class Product extends Model implements HasMedia, Viewable
{
/**
* Has factory (laravel default)
*/
use HasFactory;
/**
* Has Schemaless Attributes (spatie/laravel-schemaless-attributes)
*/
use HasSchemalessAttributes;
/**
* Has Slug (spatie/laravel-sluggable)
*/
use HasSlug;
/**
* Interacts with media (spatie/laravel-medialibrary)
*/
use InteractsWithMedia;
/**
* Interacts with views (cyrildewit/eloquent-viewable)
*/
use InteractsWithViews;
/**
* Product helpers for front end
*/
use ProductFrontEndHelpers;
/**
* Media interactions
*/
use ProductMedia {
ProductMedia::registerMediaCollections insteadof InteractsWithMedia;
ProductMedia::registerMediaConversions insteadof InteractsWithMedia;
}
/**
* Product properties (attributes[EAV])
*/
use ProductProperties;
/**
* Product relationships
*/
use ProductRelationships;
/**
* Product scope queries
*/
use ProductScopeQueries;
/**
* Product states (new, featured, ...)
*/
use ProductStates;
/**
* Products stocks
*/
use ProductStock;
/**
* Interacts with media (whitecube/nova-flexible-content)
*/
// use HasFlexible;
/**
* Product Data Transformers
*/
use ProductTransformers;
/**
* The attributes that are mass assignable.
*/
protected $fillable = [
'id',
'parent_id',
'brand_id',
'name',
'slug',
'description',
'sku',
'barcode',
'stock',
'security_stock',
'cost_amount',
'price_amount',
'old_price_amount',
'seo_title',
'seo_description',
'options',
'is_visible',
'colour',
'size',
];
}