¨4.0.1¨

This commit is contained in:
¨NW¨
2023-12-03 14:07:47 +00:00
parent c08b36d1b6
commit f35052522d
1112 changed files with 43019 additions and 24987 deletions

View File

@@ -2,9 +2,15 @@
namespace Modules\Product\Http\Controllers\Admin;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\View\View;
use Modules\Product\Entities\Product;
use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Application;
use Modules\Admin\Traits\HasCrudActions;
use Modules\Product\Http\Requests\SaveProductRequest;
use Modules\Product\Transformers\ProductEditResource;
class ProductController
{
@@ -15,26 +21,124 @@ class ProductController
*
* @var string
*/
protected $model = Product::class;
protected string $model = Product::class;
/**
* Label of the resource.
*
* @var string
*/
protected $label = 'product::products.product';
protected string $label = 'product::products.product';
/**
* View path of the resource.
*
* @var string
*/
protected $viewPath = 'product::admin.products';
protected string $viewPath = 'product::admin.products';
/**
* Form requests for the resource.
*
* @var array|string
*/
protected $validation = SaveProductRequest::class;
protected string|array $validation = SaveProductRequest::class;
/**
* Store a newly created resource in storage.
*
* @return Response|JsonResponse
*/
public function store()
{
$this->disableSearchSyncing();
$entity = $this->getModel()->create(
$this->getRequest('store')->all()
);
$this->searchable($entity);
$message = trans('admin::messages.resource_created', ['resource' => $this->getLabel()]);
if (request()->query('exit_flash')) {
session()->flash('exit_flash', $message);
}
if (request()->wantsJson()) {
return response()->json(
[
'success' => true,
'message' => $message,
'product_id' => $entity->id,
], 200
);
}
return redirect()->route("{$this->getRoutePrefix()}.index")
->withSuccess($message);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
*
* @return Factory|View|Application
*/
public function edit($id): Factory|View|Application
{
$entity = $this->getEntity($id);
$productEditResource = new ProductEditResource($entity);
return view("{$this->viewPath}.edit",
[
'product' => $entity,
'product_resource' => $productEditResource->response()->content(),
]
);
}
/**
* Update the specified resource in storage.
*
* @param int $id
*/
public function update($id)
{
$entity = $this->getEntity($id);
$this->disableSearchSyncing();
$entity->update(
$this->getRequest('update')->all()
);
$entity->withoutEvents(function () use ($entity) {
$entity->touch();
});
$productEditResource = new ProductEditResource($entity);
$this->searchable($entity);
$message = trans('admin::messages.resource_updated', ['resource' => $this->getLabel()]);
if (request()->query('exit_flash')) {
session()->flash('exit_flash', $message);
}
if (request()->wantsJson()) {
return response()->json(
[
'success' => true,
'message' => $message,
'product_resource' => $productEditResource,
], 200
);
}
}
}

View File

@@ -2,11 +2,17 @@
namespace Modules\Product\Http\Controllers;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Routing\Controller;
use Modules\Review\Entities\Review;
use Illuminate\Contracts\View\View;
use Modules\Product\Entities\Product;
use Illuminate\Contracts\View\Factory;
use Modules\Product\Events\ProductViewed;
use Modules\Product\Filters\ProductFilter;
use Illuminate\Contracts\Foundation\Application;
use Modules\Product\Repositories\ProductRepository;
use Modules\Product\Http\Middleware\SetProductSortOption;
class ProductController extends Controller
@@ -23,12 +29,14 @@ class ProductController extends Controller
$this->middleware(SetProductSortOption::class)->only('index');
}
/**
* Display a listing of the resource.
*
* @param \Modules\Product\Entities\Product $model
* @param \Modules\Product\Filters\ProductFilter $productFilter
* @return \Illuminate\Http\Response
* @param Product $model
* @param ProductFilter $productFilter
*
* @return JsonResponse|Application|Factory|View
*/
public function index(Product $model, ProductFilter $productFilter)
{
@@ -39,28 +47,37 @@ class ProductController extends Controller
return view('public.products.index');
}
/**
* Show the specified resource.
*
* @param string $slug
* @return \Illuminate\Http\Response
*
* @return Response
*/
public function show($slug)
{
$product = Product::findBySlug($slug);
$product = ProductRepository::findBySlug($slug);
$relatedProducts = $product->relatedProducts()->forCard()->get();
$upSellProducts = $product->upSellProducts()->forCard()->get();
$review = $this->getReviewData($product);
$product->append([
'is_in_flash_sale',
'flash_sale_end_date',
'formatted_price_range',
]);
event(new ProductViewed($product));
return view('public.products.show', compact('product', 'relatedProducts', 'upSellProducts', 'review'));
}
private function getReviewData(Product $product)
{
if (! setting('reviews_enabled')) {
return;
if (!setting('reviews_enabled')) {
return null;
}
return Review::countAndAvgRating($product);

View File

@@ -13,7 +13,8 @@ class ProductPriceController
* Show the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*
* @return string
*/
public function show($id)
{
@@ -23,12 +24,12 @@ class ProductPriceController
->findOrFail($id);
$variantPrice = $this->cartItem($product, request('options', []))
->total()
->totalPrice()
->convertToCurrentCurrency()
->format();
return product_price_formatted($product, function ($price) use ($product, $variantPrice) {
if (! $product->hasSpecialPrice()) {
if (!$product->hasSpecialPrice()) {
return $variantPrice;
}
@@ -36,6 +37,7 @@ class ProductPriceController
});
}
private function cartItem(Product $product, array $options)
{
$chosenOptions = new ChosenProductOptions($product, $options);

View File

@@ -2,6 +2,7 @@
namespace Modules\Product\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Modules\Product\Entities\Product;
use Modules\Category\Entities\Category;
@@ -14,9 +15,10 @@ trait ProductSearch
/**
* Search products for the request.
*
* @param \Modules\Product\Entities\Product $model
* @param \Modules\Product\Filters\ProductFilter $productFilter
* @return \Illuminate\Http\Response
* @param Product $model
* @param ProductFilter $productFilter
*
* @return JsonResponse
*/
public function searchProducts(Product $model, ProductFilter $productFilter)
{
@@ -43,9 +45,10 @@ trait ProductSearch
]);
}
private function getAttributes($productIds)
{
if (! request()->filled('category') || $this->filteringViaRootCategory()) {
if (!request()->filled('category') || $this->filteringViaRootCategory()) {
return collect();
}
@@ -57,6 +60,7 @@ trait ProductSearch
->get();
}
private function filteringViaRootCategory()
{
return Category::where('slug', request('category'))
@@ -64,6 +68,7 @@ trait ProductSearch
->isRoot();
}
private function getProductsCategoryIds($productIds)
{
return DB::table('product_categories')

View File

@@ -2,8 +2,10 @@
namespace Modules\Product\Http\Controllers;
use Closure;
use Modules\Product\Entities\Product;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Modules\Product\Http\Response\SuggestionsResponse;
class SuggestionController
@@ -11,9 +13,9 @@ class SuggestionController
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
* @return SuggestionsResponse
*/
public function index(Product $model)
public function index(Product $model): SuggestionsResponse
{
$products = $this->getProducts($model);
@@ -25,25 +27,13 @@ class SuggestionController
);
}
/**
* Get total results count.
*
* @param \Modules\Product\Entities\Product $model
* @return int
*/
private function getTotalResults(Product $model)
{
return $model->search(request('query'))
->query()
->when(request()->filled('category'), $this->categoryQuery())
->count();
}
/**
* Get products suggestions.
*
* @param \Modules\Product\Entities\Product $model
* @return \Illuminate\Database\Eloquent\Collection
* @param Product $model
*
* @return Collection
*/
private function getProducts(Product $model)
{
@@ -67,10 +57,11 @@ class SuggestionController
->get();
}
/**
* Returns categories condition closure.
*
* @return \Closure
* @return Closure
*/
private function categoryQuery()
{
@@ -80,4 +71,20 @@ class SuggestionController
});
};
}
/**
* Get totalPrice results count.
*
* @param Product $model
*
* @return int
*/
private function getTotalResults(Product $model): int
{
return $model->search(request('query'))
->query()
->when(request()->filled('category'), $this->categoryQuery())
->count();
}
}

View File

@@ -10,8 +10,9 @@ class SetProductSortOption
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param Request $request
* @param Closure $next
*
* @return mixed
*/
public function handle(Request $request, Closure $next)
@@ -27,25 +28,29 @@ class SetProductSortOption
return $next($request);
}
/**
* Determine if the request should set "relevance" sort option.
*
* @param \Illuminate\Http\Request $request
* @param Request $request
*
* @return void
*/
private function shouldSetRelevanceSortOption($request)
{
return $request->has('query') && ! $request->has('sort');
return $request->has('query') && !$request->has('sort');
}
/**
* Determine if the request should set "latest" sort option.
*
* @param \Illuminate\Http\Request $request
* @param Request $request
*
* @return bool
*/
private function shouldSetLatestSortOption($request)
{
return ! $request->has('query') && ! $request->has('sort');
return !$request->has('query') && !$request->has('sort');
}
}

View File

@@ -3,8 +3,11 @@
namespace Modules\Product\Http\Requests;
use Illuminate\Validation\Rule;
use Modules\Option\Entities\Option;
use Modules\Product\Entities\Product;
use Modules\Core\Http\Requests\Request;
use Modules\Variation\Entities\Variation;
use Modules\Product\Rules\DistinctProductVariationValueLabel;
class SaveProductRequest extends Request
{
@@ -15,35 +18,126 @@ class SaveProductRequest extends Request
*/
protected $availableAttributes = 'product::attributes';
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
public function rules(): array
{
return array_merge(
$this->getProductRules(),
$this->getProductAttributeRules(),
$this->getProductVariationsRules(),
$this->getProductVariantsRules(),
$this->getProductOptionsRules(),
);
}
public function getProductRules(): array
{
return array_merge(
[
'slug' => $this->getSlugRules(),
'name' => 'required',
'description' => 'required',
'brand_id' => ['nullable', Rule::exists('brands', 'id')],
'tax_class_id' => ['nullable', Rule::exists('tax_classes', 'id')],
'price' => 'required_without:variants|nullable|numeric|min:0|max:99999999999999',
'special_price' => 'nullable|numeric|min:0|max:99999999999999',
'special_price_type' => ['nullable', Rule::in(['fixed', 'percent'])],
'special_price_start' => 'nullable|date|before:special_price_end',
'special_price_end' => 'nullable|date|after:special_price_start',
'manage_stock' => 'required|boolean',
'qty' => 'required_if:manage_stock,1|nullable|numeric',
'in_stock' => 'required|boolean',
'new_from' => 'nullable|date',
'new_to' => 'nullable|date',
'is_virtual' => 'required|boolean',
'is_active' => 'required|boolean',
],
$this->getInventoryRules()
);
}
public function getInventoryRules(): array
{
if (!$this->request->has('variations')) {
return [
'manage_stock' => 'required|boolean',
'qty' => 'required_if:manage_stock,1|nullable|numeric',
'in_stock' => 'required|boolean',
];
}
return [];
}
public function getProductAttributeRules(): array
{
return [
'slug' => $this->getSlugRules(),
'name' => 'required',
'description' => 'required',
'brand_id' => ['nullable', Rule::exists('brands', 'id')],
'tax_class_id' => ['nullable', Rule::exists('tax_classes', 'id')],
'is_virtual' => 'required|boolean',
'is_active' => 'required|boolean',
'price' => 'required|numeric|min:0|max:99999999999999',
'special_price' => 'nullable|numeric|min:0|max:99999999999999',
'special_price_type' => ['nullable', Rule::in(['fixed', 'percent'])],
'special_price_start' => 'nullable|date',
'special_price_end' => 'nullable|date',
'manage_stock' => 'required|boolean',
'qty' => 'required_if:manage_stock,1|nullable|numeric',
'in_stock' => 'required|boolean',
'new_from' => 'nullable|date',
'new_to' => 'nullable|date',
'attributes.*.attribute_id' => ['required_with:attributes.*.values', Rule::exists('attributes', 'id')],
'attributes.*.values' => ['required_with:attributes.*.attribute_id', Rule::exists('attribute_values', 'id')],
];
}
private function getSlugRules()
public function getProductVariationsRules(): array
{
return [
'variations.*.name' => 'required_with:variations.*.type',
'variations.*.type' => ['nullable', 'required_with:variations.*.name', Rule::in(Variation::TYPES)],
'variations.*.values.*.label' => ['required_with:variations.*.type', new DistinctProductVariationValueLabel()],
'variations.*.values.*.color' => ['required_if:variations.*.type,color', 'regex:/^#(?:[0-9a-fA-F]{3}){1,2}$/'],
'variations.*.values.*.image' => 'required_if:type,image|integer|min:1',
];
}
public function getProductVariantsRules(): array
{
return [
'variants.*.name' => 'required',
'variants.*.sku' => 'nullable',
'variants.*.price' => 'required_if:variants.*.is_active,true|nullable|numeric|min:0|max:99999999999999',
'variants.*.special_price' => 'nullable|numeric|min:0|max:99999999999999',
'variants.*.special_price_type' => ['nullable', Rule::in(['fixed', 'percent'])],
'variants.*.special_price_start' => 'nullable|date|before:variants.*.special_price_end',
'variants.*.special_price_end' => 'nullable|date|after:variants.*.special_price_start',
'variants.*.manage_stock' => 'required_if:variants.*.is_active,1|boolean',
'variants.*.qty' => 'required_if:variants.*.is_active,1|required_if:variants.*.manage_stock,1|nullable|numeric',
'variants.*.in_stock' => 'required_if:variants.*.is_active,1|boolean',
'variants.*.is_active' => 'required|boolean',
];
}
public function getProductOptionsRules(): array
{
return [
'options.*.name' => 'required_with:options.*.type',
'options.*.type' => ['nullable', 'required_with:options.*.name', Rule::in(Option::TYPES)],
'options.*.is_required' => ['required_with:options.*.name', 'boolean'],
'options.*.values.*.label' => 'required_if:options.*.type,dropdown,checkbox,checkbox_custom,radio,radio_custom,multiple_select',
'options.*.values.*.price' => 'nullable|numeric|min:0|max:99999999999999',
'options.*.values.*.price_type' => ['required', Rule::in(['fixed', 'percent'])],
];
}
public function messages()
{
return array_merge(parent::messages(), [
'price.required_without' => trans('product::validation.price_field_is_required'),
]);
}
private function getSlugRules(): array
{
$rules = $this->route()->getName() === 'admin.products.update' ? ['required'] : ['sometimes'];

View File

@@ -2,6 +2,8 @@
namespace Modules\Product\Http\Response;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Modules\Product\Entities\Product;
use Modules\Category\Entities\Category;
@@ -9,20 +11,21 @@ use Illuminate\Contracts\Support\Responsable;
class SuggestionsResponse implements Responsable
{
private $query;
private $products;
private $categories;
private $totalResults;
private string $query;
private Collection $products;
private Collection $categories;
private int $totalResults;
/**
* Create a new instance.
*
* @param string $query
* @param int $totalResults
* @param \Illuminate\Support\Collection $products
* @param \Illuminate\Support\Collection $categories
* @param Collection $products
* @param Collection $categories
*/
public function __construct($query, Collection $products, Collection $categories, $totalResults)
public function __construct(string $query, Collection $products, Collection $categories, int $totalResults)
{
$this->query = $query;
$this->products = $products;
@@ -30,13 +33,15 @@ class SuggestionsResponse implements Responsable
$this->totalResults = $totalResults;
}
/**
* Create an HTTP response that represents the object.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @param Request $request
*
* @return JsonResponse
*/
public function toResponse($request)
public function toResponse($request): JsonResponse
{
return response()->json([
'categories' => $this->transformCategories(),
@@ -45,12 +50,13 @@ class SuggestionsResponse implements Responsable
]);
}
/**
* Transform the categories.
*
* @return \Illuminate\Support\Collection
* @return Collection
*/
private function transformCategories()
private function transformCategories(): Collection
{
return $this->categories->map(function (Category $category) {
return [
@@ -61,44 +67,48 @@ class SuggestionsResponse implements Responsable
})->unique('slug')->values();
}
/**
* Transform the products.
*
* @return \Illuminate\Support\Collection
* @return Collection
*/
private function transformProducts()
private function transformProducts(): Collection
{
return $this->products->map(function (Product $product) {
return [
'slug' => $product->slug,
'name' => $this->highlight($product->name),
'formatted_price' => $product->getFormattedPriceAttribute(),
'base_image' => $product->getBaseImageAttribute(),
'is_out_of_stock' => $product->isOutOfStock(),
'url' => $product->url(),
'formatted_price' => $product->variant?->formatted_price ?? $product->formatted_price,
'base_image' => $product->variant?->base_image ?? $product->base_image,
'is_out_of_stock' => $product->variant?->isOutOfStock() ?? $product->isOutOfStock(),
'url' => $product->variant?->url() ?? $product->url(),
];
});
}
/**
* Highlight the given text.
*
* @param string $text
*
* @return string
*/
private function highlight($text)
private function highlight($text): string
{
$query = str_replace(' ', '|', preg_quote($this->query));
return preg_replace("/($query)/i", '<em>$1</em>', $text);
}
/**
* Get remaining results count.
*
* @return int
*/
private function getRemainingCount()
private function getRemainingCount(): int
{
return $this->totalResults - $this->products->count();
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Modules\Product\Http\ViewComposers;
use Illuminate\View\View;
use Modules\Tag\Entities\Tag;
use Modules\Brand\Entities\Brand;
use Modules\Tax\Entities\TaxClass;
use Modules\Option\Entities\Option;
use Modules\Category\Entities\Category;
use Modules\Variation\Entities\Variation;
use Modules\Attribute\Entities\AttributeSet;
class ProductCreatePageComposer
{
/**
* Bind data to the view.
*
* @param View $view
*
* @return void
*/
public function compose(View $view)
{
$view->with([
'globalVariations' => Variation::globals()->latest()->get(),
'globalOptions' => Option::globals()->latest()->get(),
'brands' => Brand::list()->prepend(trans('admin::admin.form.please_select'), ''),
'categories' => Category::treeList(),
'taxClasses' => TaxClass::list()->prepend(trans('admin::admin.form.please_select'), ''),
'tags' => Tag::list(),
'attributeSets' => AttributeSet::with('attributes.values')->get()->sortBy('name'),
]);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Modules\Product\Http\ViewComposers;
use Illuminate\View\View;
use Modules\Tag\Entities\Tag;
use Modules\Brand\Entities\Brand;
use Modules\Tax\Entities\TaxClass;
use Modules\Option\Entities\Option;
use Modules\Category\Entities\Category;
use Modules\Variation\Entities\Variation;
use Modules\Attribute\Entities\AttributeSet;
class ProductEditPageComposer
{
/**
* Bind data to the view.
*
* @param View $view
*
* @return void
*/
public function compose(View $view)
{
$view->with([
'globalVariations' => Variation::globals()->latest()->get(),
'globalOptions' => Option::globals()->latest()->get(),
'brands' => Brand::list()->prepend(trans('admin::admin.form.please_select'), ''),
'categories' => Category::treeList(),
'taxClasses' => TaxClass::list()->prepend(trans('admin::admin.form.please_select'), ''),
'tags' => Tag::list(),
'attributeSets' => AttributeSet::with('attributes.values')->get()->sortBy('name'),
]);
}
}