| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424 |
- <?php
- namespace Webkul\BagistoApi\Models;
- use ApiPlatform\Metadata\ApiProperty;
- use ApiPlatform\Metadata\ApiResource;
- use ApiPlatform\Metadata\Get;
- use ApiPlatform\Metadata\GraphQl\Mutation;
- use ApiPlatform\Metadata\GraphQl\Query;
- use ApiPlatform\Metadata\GraphQl\QueryCollection;
- use ApiPlatform\Metadata\Post;
- use ApiPlatform\Metadata\Put;
- use ApiPlatform\OpenApi\Model;
- use Illuminate\Database\Eloquent\Relations\BelongsTo;
- use Illuminate\Database\Eloquent\Relations\BelongsToMany;
- use Illuminate\Database\Eloquent\Relations\HasMany;
- use Symfony\Component\Serializer\Annotation\Groups;
- use Webkul\BagistoApi\Http\Requests\Admin\ProductFormRequest;
- use Webkul\BagistoApi\Resolver\SingleProductBagistoApiResolver;
- use Webkul\BagistoApi\State\ProductBagistoApiProvider;
- use Webkul\BagistoApi\State\ProductGraphQLProvider;
- use Webkul\BagistoApi\State\ProductProcessor;
- use Webkul\Product\Models\Product as BaseProduct;
- use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
- use Longyi\Core\Models\ProductOption;
- use Longyi\Core\Models\ProductOptionValue;
- #[ApiResource(
- routePrefix: '/api/shop',
- operations: [
- new Get,
- new Post(
- security: "is_granted('CREATE_PRODUCT')",
- processor: ProductProcessor::class,
- // rules: ProductFormRequest::class,
- openapi: new Model\Operation(
- summary: 'Store the product',
- description: 'Product creation endpoint',
- tags: ['Product'],
- parameters: [],
- requestBody: new Model\RequestBody(
- description: 'Product creation payload',
- required: true,
- content: new \ArrayObject([
- 'application/json' => [
- 'schema' => [
- 'type' => 'object',
- 'properties' => [
- 'type' => [
- 'type' => 'string',
- 'example' => 'simple',
- ],
- 'attribute_family_id' => [
- 'type' => 'integer',
- 'example' => 1,
- ],
- 'sku' => [
- 'type' => 'string',
- 'example' => 'furniture',
- ],
- 'super_attributes' => [
- 'type' => 'object',
- 'example' => [
- 'color' => [1],
- 'size' => [6],
- ],
- ],
- ],
- ],
- 'examples' => [
- 'simple_product' => [
- 'summary' => 'Simple Product',
- 'description' => 'Create a standard simple product',
- 'value' => [
- 'type' => 'simple',
- 'attribute_family_id' => 1,
- 'sku' => 'furniture',
- ],
- ],
- 'configurable_product' => [
- 'summary' => 'Configurable Product',
- 'description' => 'Create a configurable product with variations',
- 'value' => [
- 'type' => 'configurable',
- 'attribute_family_id' => 1,
- 'sku' => 'furniture',
- 'super_attributes' => [
- 'color' => [1],
- 'size' => [6],
- ],
- ],
- ],
- ],
- ],
- ]),
- ),
- ),
- ),
- new Put(
- security: "is_granted('EDIT_PRODUCT')",
- processor: ProductProcessor::class,
- openapi: new Model\Operation(
- summary: 'Update the product',
- description: 'Product update endpoint',
- tags: ['Product'],
- parameters: [],
- requestBody: new Model\RequestBody(
- description: 'Product update payload',
- required: true,
- content: new \ArrayObject([
- 'application/json' => [
- 'schema' => [
- 'type' => 'object',
- 'properties' => [
- 'type' => [
- 'type' => 'string',
- 'example' => 'simple',
- ],
- 'attribute_family_id' => [
- 'type' => 'integer',
- 'example' => 1,
- ],
- 'sku' => [
- 'type' => 'string',
- 'example' => 'furniture',
- ],
- 'super_attributes' => [
- 'type' => 'object',
- 'example' => [
- 'color' => [1],
- 'size' => [6],
- ],
- ],
- ],
- ],
- 'examples' => [
- 'simple_product' => [
- 'summary' => 'Simple Product',
- 'description' => 'Create a standard simple product',
- 'value' => [
- 'channel' => 'default',
- 'locale' => 'en',
- 'sku' => 'furniture',
- 'product_number' => 'ssf-001',
- 'name' => 'Sofa Set',
- 'url_key' => 'sofa-set-furniture',
- 'tax_category_id' => null,
- 'new' => 1,
- 'featured' => 1,
- 'manage_stock' => 1,
- 'visible_individually' => 1,
- 'guest_checkout' => 0,
- 'status' => 1,
- 'color' => 3,
- 'size' => 9,
- 'brand' => 17,
- 'short_description' => 'What is Lorem Ipsum?',
- 'description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
- 'meta_title' => 'Premium sofa sets',
- 'meta_description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
- 'meta_keywords' => 'Sofa set',
- 'price' => 10.5,
- 'cost' => 0,
- 'special_price' => 8.3,
- 'special_price_from' => '2023-05-30',
- 'special_price_to' => '2025-05-22',
- 'length' => 0,
- 'width' => 0,
- 'height' => 0,
- 'weight' => 1,
- 'inventories' => [
- '1' => 500,
- ],
- 'images' => [
- 'files' => [],
- 'position' => [1],
- ],
- 'videos' => [
- 'files' => [],
- 'position' => [1],
- ],
- 'categories' => [1],
- 'channels' => [1],
- 'up_sell' => [1],
- 'cross_sell' => [1],
- 'related_products' => [1],
- ],
- ],
- 'configurable_product' => [
- 'summary' => 'Configurable Product',
- 'description' => 'Update a configurable product with variations',
- 'value' => [
- 'channel' => 'default',
- 'locale' => 'en',
- 'sku' => 'skipping-rope',
- 'product_number' => 'sr-001',
- 'name' => 'Skipping Rope',
- 'url_key' => 'skipping-rope',
- 'tax_category_id' => null,
- 'new' => 1,
- 'featured' => 1,
- 'visible_individually' => 1,
- 'guest_checkout' => 0,
- 'status' => 1,
- 'brand' => 17,
- 'short_description' => 'What is Lorem Ipsum?',
- 'description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
- 'meta_title' => 'Premium sofa sets',
- 'meta_description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
- 'meta_keywords' => 'Sofa set',
- 'price' => 0,
- 'customer_group_prices' => [
- 'customer_group_price_0' => [
- 'customer_group_id' => 1,
- 'qty' => 2,
- 'value_type' => 'fixed',
- 'value' => 3.2,
- ],
- ],
- 'categories' => [
- 1,
- 2,
- ],
- 'channels' => [
- 1,
- 3,
- 4,
- ],
- 'variants' => [
- '28' => [
- 'sku' => 'skipping-rope-variant-1-6',
- 'name' => 'Red-S',
- 'color' => 1,
- 'size' => 6,
- 'price' => 10.5,
- 'weight' => 1.2,
- 'status' => 1,
- 'inventories' => [
- '1' => 500,
- ],
- 'images[]' => [
- 'string',
- ],
- ],
- '29' => [
- 'sku' => 'skipping-rope-variant-1-7',
- 'name' => 'Red-M',
- 'color' => 1,
- 'size' => 7,
- 'price' => 15,
- 'weight' => 1,
- 'status' => 1,
- 'inventories' => [
- '1' => 500,
- ],
- 'images[files]' => [
- 'string',
- ],
- ],
- ],
- ],
- ],
- 'downloadable_product' => [
- 'summary' => 'Downloadable Product',
- 'description' => 'Update a downloadable product with links and samples',
- 'value' => [
- 'channel' => 'default',
- 'locale' => 'en',
- 'sku' => 'skipping-rope',
- 'product_number' => 'sr-001',
- 'name' => 'Skipping Rope',
- 'url_key' => 'skipping-rope',
- 'tax_category_id' => null,
- 'new' => 1,
- 'featured' => 1,
- 'visible_individually' => 1,
- 'guest_checkout' => 0,
- 'status' => 1,
- 'brand' => 17,
- 'short_description' => 'What is Lorem Ipsum?',
- 'description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
- 'meta_title' => 'Premium sofa sets',
- 'meta_description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
- 'meta_keywords' => 'Sofa set',
- 'price' => 0,
- 'customer_group_prices' => [
- 'customer_group_price_0' => [
- 'customer_group_id' => 1,
- 'qty' => 2,
- 'value_type' => 'fixed',
- 'value' => 3.2,
- ],
- ],
- 'categories' => [
- 1,
- 2,
- ],
- 'channels' => [
- 1,
- 3,
- 4,
- ],
- 'downloadable_links' => [
- 'link_0' => [
- 'en' => [
- 'title' => 'Link 1',
- ],
- 'price' => 5,
- 'type' => 'url',
- 'url' => 'https://cdn.pixabay.com/photo/2016/03/26/13/08/conceptual-1280533_1280.jpg',
- 'sample_type' => 'url',
- 'sample_url' => 'https://cdn.pixabay.com/photo/2016/11/22/19/11/brick-wall-1850095_1280.jpg',
- 'downloads' => 10,
- 'sort_order' => 1,
- ],
- 'link_1' => [
- 'en' => [
- 'title' => 'Link 2',
- ],
- 'price' => 10,
- 'type' => 'url',
- 'url' => 'https://cdn.pixabay.com/photo/2016/03/26/13/08/conceptual-1280533_1280.jpg',
- 'sample_type' => 'url',
- 'sample_url' => 'https://cdn.pixabay.com/photo/2016/11/22/19/11/brick-wall-1850095_1280.jpg',
- 'downloads' => 20,
- 'sort_order' => 2,
- ],
- ],
- 'downloadable_samples' => [
- 'sample_0' => [
- 'en' => [
- 'title' => 'Sample 1',
- ],
- 'type' => 'url',
- 'url' => 'https://cdn.pixabay.com/photo/2017/10/04/14/50/staging-2816464_1280.jpg',
- 'sort_order' => 1,
- ],
- 'sample_1' => [
- 'en' => [
- 'title' => 'Sample 2',
- ],
- 'type' => 'url',
- 'url' => 'https://cdn.pixabay.com/photo/2015/12/05/23/38/nursery-1078923_1280.jpg',
- 'sort_order' => 2,
- ],
- ],
- ],
- ],
- 'grouped_product' => [
- 'summary' => 'Group Product',
- 'description' => 'Update a grouped product along with its associated products',
- 'value' => [
- 'channel' => 'default',
- 'locale' => 'en',
- 'sku' => 'skipping-rope',
- 'product_number' => 'sr-001',
- 'name' => 'Skipping Rope',
- 'url_key' => 'skipping-rope',
- 'tax_category_id' => null,
- 'new' => 1,
- 'featured' => 1,
- 'visible_individually' => 1,
- 'guest_checkout' => 0,
- 'status' => 1,
- 'brand' => 17,
- 'short_description' => 'What is Lorem Ipsum?',
- 'description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
- 'meta_title' => 'Premium sofa sets',
- 'meta_description' => 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
- 'meta_keywords' => 'Sofa set',
- 'price' => 0,
- 'customer_group_prices' => [
- 'customer_group_price_0' => [
- 'customer_group_id' => 1,
- 'qty' => 2,
- 'value_type' => 'fixed',
- 'value' => 3.2,
- ],
- ],
- 'categories' => [
- 1,
- 2,
- ],
- 'channels' => [
- 1,
- 3,
- 4,
- ],
- 'links' => [
- 'link_0' => [
- 'associated_product_id' => 1,
- 'qty' => 2,
- 'sort_order' => 1,
- ],
- 'link_1' => [
- 'associated_product_id' => 2,
- 'qty' => 3,
- 'sort_order' => 2,
- ],
- ],
- ],
- ],
- ],
- ],
- ]),
- ),
- ),
- ),
- ],
- graphQlOperations: [
- new Mutation(
- name: 'create',
- processor: \Webkul\BagistoApi\State\ProductProcessor::class,
- denormalizationContext: [
- 'allow_extra_attributes' => true,
- 'groups' => ['mutation'],
- ],
- ),
- new Mutation(
- name: 'update',
- processor: \Webkul\BagistoApi\State\ProductProcessor::class,
- ),
- new Query(
- args: [
- 'id' => ['type' => 'ID'],
- 'sku' => ['type' => 'String'],
- 'urlKey' => ['type' => 'String'],
- 'locale' => ['type' => 'String', 'description' => 'Locale code for localized data (e.g. "en", "fr")'],
- 'channel' => ['type' => 'String', 'description' => 'Channel code (e.g. "default")'],
- ],
- resolver: SingleProductBagistoApiResolver::class
- ),
- ]
- )]
- #[ApiResource(
- routePrefix: '/api/shop',
- shortName: 'Product',
- uriTemplate: '/products-collection',
- operations: [
- ],
- graphQlOperations: [
- new Query(resolver: BaseQueryItemResolver::class),
- new QueryCollection(
- provider: ProductGraphQLProvider::class,
- args: [
- 'sortKey' => [
- 'type' => 'String',
- 'description' => 'Sort products by field (TITLE, CREATED_AT, UPDATED_AT, PRICE, etc.)',
- ],
- 'reverse' => [
- 'type' => 'Boolean',
- 'description' => 'Reverse the sort order (true = descending, false = ascending)',
- ],
- 'query' => [
- 'type' => 'String',
- 'description' => 'Search query to filter products by SKU or name',
- ],
- 'filter' => [
- 'type' => 'String',
- 'description' => 'JSON filter object containing attribute filters (type, sku, category_id, price, color, name, etc.). Example: {"type":"configurable","sku":"ABC123"}',
- ],
- 'first' => [
- 'type' => 'Int',
- 'description' => 'Limit the number of products returned',
- ],
- 'after' => [
- 'type' => 'String',
- 'description' => 'Relay cursor for forward pagination',
- ],
- 'before' => [
- 'type' => 'String',
- 'description' => 'Relay cursor for backward pagination',
- ],
- 'last' => [
- 'type' => 'Int',
- 'description' => 'Return the last N items (used with before cursor)',
- ],
- 'locale' => [
- 'type' => 'String',
- 'description' => 'Fetch data products by locale',
- ],
- 'channel' => [
- 'type' => 'String',
- 'description' => 'Fetch data products by channel',
- ],
- ]
- ),
- ]
- )]
- class Product extends BaseProduct
- {
- public $locale;
- public $channel;
- /**
- * Keep the polymorphic type aligned with the base class used by the
- * Price indexer (Webkul\Product\Models\Product), so morphMany relations
- * like price_indices() resolve the rows that were written with
- * priceable_type = Webkul\Product\Models\Product.
- */
- public function getMorphClass(): string
- {
- return BaseProduct::class;
- }
- protected $with = [
- 'attribute_family',
- 'images',
- 'attribute_values',
- 'super_attributes',
- 'variants',
- 'price_indices',
- 'flexibleVariants',
- 'options',
- ];
- protected $appends = [
- 'name', 'description', 'short_description', 'price', 'special_price',
- 'weight', 'product_number', 'status', 'new', 'featured',
- 'visible_individually', 'guest_checkout', 'manage_stock',
- 'url_key', 'tax_category_id', 'special_price_from', 'special_price_to',
- 'meta_title', 'meta_keywords',
- 'cost', 'meta_description', 'length', 'width', 'height',
- 'color', 'size', 'brand', 'locale', 'channel', 'description_html',
- 'minimum_price', 'maximum_price', 'regular_minimum_price', 'regular_maximum_price',
- 'formatted_price', 'formatted_special_price', 'formatted_minimum_price',
- 'formatted_maximum_price', 'formatted_regular_minimum_price', 'formatted_regular_maximum_price',
- 'index', 'combinations', 'super_attribute_options',
- 'product_options',
- ];
- protected static array $systemAttributes = [
- 'sku' => ['id' => 1],
- 'name' => ['id' => 2],
- 'url_key' => ['id' => 3],
- 'tax_category_id' => ['id' => 4],
- 'new' => ['id' => 5],
- 'featured' => ['id' => 6],
- 'visible_individually' => ['id' => 7],
- 'status' => ['id' => 8],
- 'short_description' => ['id' => 9],
- 'description' => ['id' => 10],
- 'price' => ['id' => 11],
- 'special_price' => ['id' => 13],
- 'special_price_from' => ['id' => 14],
- 'special_price_to' => ['id' => 15],
- 'meta_title' => ['id' => 16],
- 'meta_keywords' => ['id' => 17],
- 'weight' => ['id' => 22],
- 'guest_checkout' => ['id' => 26],
- 'product_number' => ['id' => 27],
- 'manage_stock' => ['id' => 28],
- 'cost' => ['id' => 12],
- 'meta_description' => ['id' => 18],
- 'length' => ['id' => 19],
- 'width' => ['id' => 20],
- 'height' => ['id' => 21],
- 'color' => ['id' => 23],
- 'size' => ['id' => 24],
- 'brand' => ['id' => 25],
- ];
- #[ApiProperty(identifier: true, writable: false)]
- public function getId(): ?int
- {
- return $this->id;
- }
- /** Parent */
- #[ApiProperty(writable: true, readable: true, required: false)]
- public function getParent(): mixed
- {
- return $this->parent_id;
- }
- public function setParent(mixed $value): void
- {
- // Parent cannot be modified via API
- }
- /** Is Saleable */
- #[ApiProperty(
- writable: false,
- readable: true
- )]
- public function getIsSaleableAttribute(): bool
- {
- return parent::isSaleable();
- }
- public function isSaleable(): bool
- {
- return $this->getIsSaleableAttribute();
- }
- public function availableForSaleAttribute(): bool
- {
- return $this->status && $this->isSaleable();
- }
- /** Available for Sale */
- #[ApiProperty(
- writable: false,
- readable: true
- )]
- public function availableForSale(): bool
- {
- return $this->status && $this->isSaleable();
- }
- public function attribute_values(): HasMany
- {
- return $this->hasMany(AttributeValue::class, 'product_id');
- }
- /**
- * Get locale context.
- */
- /**
- * Get locale attribute.
- */
- public function getLocaleAttribute(): ?string
- {
- return $this->locale;
- }
- /**
- * Get locale context.
- */
- #[ApiProperty(
- writable: true,
- readable: true
- )]
- #[Groups(['mutation'])]
- public function getLocale(): ?string
- {
- return $this->getLocaleAttribute();
- }
- /**
- * Set locale context.
- */
- public function setLocale(?string $value): void
- {
- $this->locale = $value;
- }
- /**
- * Get channel attribute.
- */
- public function getChannelAttribute(): ?string
- {
- return $this->channel;
- }
- /**
- * Get channel context.
- */
- #[ApiProperty(
- writable: true,
- readable: true
- )]
- #[Groups(['mutation'])]
- public function getChannel(): ?string
- {
- return $this->getChannelAttribute();
- }
- /**
- * Set channel context.
- */
- public function setChannel(?string $value): void
- {
- $this->channel = $value;
- }
- /**
- * Get super attributes relationship (many-to-many).
- */
- public function super_attributes(): BelongsToMany
- {
- return $this->belongsToMany(Attribute::class, 'product_super_attributes');
- }
- /**
- * Get super attributes.
- */
- #[ApiProperty(
- writable: true,
- readable: true,
- required: false
- )]
- #[Groups(['mutation'])]
- public function getSuper_attributes()
- {
- return $this->super_attributes;
- }
- /**
- * Set super attributes.
- */
- public function setSuper_attributes($value): void
- {
- $this->super_attributes = $value;
- }
- /**
- * Get product categories relationship.
- */
- public function categories(): BelongsToMany
- {
- return $this->belongsToMany(Category::class, 'product_categories');
- }
- /**
- * Get product categories.
- */
- #[ApiProperty(
- writable: true,
- readable: true,
- required: false
- )]
- #[Groups(['mutation'])]
- public function getCategories()
- {
- return $this->categories;
- }
- /**
- * Set product categories.
- */
- public function setCategories($value): void
- {
- $this->categories = $value;
- }
- /**
- * The images that belong to the product.
- */
- public function images(): HasMany
- {
- return $this->hasMany(ProductImage::class, 'product_id')
- ->orderBy('position');
- }
- #[ApiProperty(
- writable: false,
- readable: true,
- required: false
- )]
- #[Groups(['mutation'])]
- public function getImages()
- {
- return $this->images;
- }
- public function setImages($value): void
- {
- $this->images = $value;
- }
- /**
- * Get product options relationship.
- */
- public function options(): BelongsToMany
- {
- return $this->belongsToMany(ProductOption::class, 'product_product_options', 'product_id', 'product_option_id')
- ->withPivot(['position', 'is_required', 'meta'])
- ->withTimestamps();
- }
- /**
- * Get product options.
- */
- #[ApiProperty(
- writable: false,
- readable: true,
- required: false
- )]
- #[Groups(['mutation'])]
- public function getOptions(): ?string
- {
- $jsonData= $this->getOptionsAttribute();
- return $jsonData !== '{}' ? $jsonData: null;
- }
- public function setOptions($value): void
- {
- $this->options = $value;
- }
- public function getOptionsAttribute(): string
- {
- if ($this->type !== 'flexible_variant') {
- return '{}';
- }
- if (! $this->relationLoaded('options')) {
- $this->load('options');
- }
- if (!$this->relationLoaded('flexible_variants')) {
- $this->load('flexible_variants');
- }
- // NOTE: Use getRelation('options') rather than $this->options — the
- // latter would recursively invoke this mutator because Eloquent's
- // attribute accessor takes precedence over the relation.
- $options = $this->getRelation('options') ?? collect();
- if ($options->isEmpty()) {
- return '{}';
- }
- $optionIds = $options->pluck('id')->toArray();
- $optionCodeMap = $options->pluck('code', 'id')->toArray();
- $index = [];
- foreach ($options as $option) {
- if (! isset($index[$option->id])) {
- $index[$option->id] = [];
- }
- if (! $option->relationLoaded('values')) {
- $option->load('values');
- }
- foreach ($option->values as $value) {
- if (in_array($value->id, $optionIds)) {
- $optionCode = $optionCodeMap[$value->id] ?? null;
- if ($optionCode) {
- $index[$option->id][$optionCode] = $value->value;
- }
- }
- }
- }
- return json_encode($index);
- }
- /**
- * Full product option relation data serialized inline as a JSON string.
- *
- * Follows the same pattern as ProductVariant::variant_images / option_values
- * — returns JSON so that ApiPlatform does not attempt IRI generation for
- * ProductOption / ProductOptionValue (they are not registered as
- * ApiResources).
- *
- * Named getProductOptionsAttribute so it is picked up by ApiPlatform's
- * EloquentPropertyNameCollectionMetadataFactory as a virtual attribute:
- * snake_case 'product_options', GraphQL field 'productOptions'.
- *
- * Returns JSON like:
- * [
- * {
- * "id": 1, "label": "Size", "code": "size", "type": "dropdown",
- * "position": 0, "is_required": true,
- * "values": [
- * {"id": 1, "label": "Small", "code": "s", "position": 0},
- * ...
- * ]
- * },
- * ...
- * ]
- * Returns null when the product has no options.
- */
- public function getProductOptionsAttribute(): ?string
- {
- if (! $this->relationLoaded('options')) {
- $this->load('options.values');
- }
- // NOTE: Use getRelation('options') — accessing $this->options would
- // recursively invoke the getOptionsAttribute mutator (which returns a
- // JSON-string variant map, not the relation Collection).
- $options = $this->getRelation('options') ?? collect();
- if ($options->isNotEmpty() && ! $options->first()->relationLoaded('values')) {
- $options->loadMissing('values');
- }
-
- $payload = $options
- ->map(function ($option) {
- return [
- 'id' => (int) $option->id,
- 'label' => $option->label,
- 'code' => $option->code,
- 'type' => $option->type,
- 'position' => (int) ($option->pivot->position ?? $option->position ?? 0),
- 'is_required' => (bool) ($option->pivot->is_required ?? false),
- 'meta' => $option->pivot->meta ?? $option->meta ?? null,
- 'values' => ($option->values ?? collect())
- ->map(function($value) {
- $isAvailable = $this->flexible_variants->filter(function ($variant) use ($value) {
- return $variant->values->contains('id', $value->id);
- })->isNotEmpty();
- if($isAvailable) {
- return [
- 'id' => (int) $value->id,
- 'label' => $value->label,
- 'code' => $value->code,
- 'position' => (int) ($value->position ?? 0),
- 'meta' => $value->meta ?? null,
- 'is_available' => $isAvailable,
- ];
- }
- return null;
- })
- ->filter(function($value) {
- return $value !== null;
- })
- ->values()
- ->all(),
- ];
- })
- ->values()
- ->all();
- return empty($payload) ? null : json_encode($payload);
- }
- /**
- * Flexible variants — overrides the parent's flexibleVariants() so that
- * BagistoApi's ProductVariant wrapper (which has variant_images / values
- * relations) is used instead of the bare Longyi\Core\Models\ProductVariant.
- */
- public function flexibleVariants(): HasMany
- {
- return $this->hasMany(ProductVariant::class, 'product_id')
- ->orderBy('sort_order');
- }
- /**
- * snake_case alias of flexibleVariants().
- *
- * ApiPlatform's Laravel PropertyAccessor accesses Eloquent models directly
- * via $model->{$snake_case_property} and bypasses Symfony's camelize logic.
- * Eloquent's isRelation() uses method_exists() with a literal key match, so
- * we need a method actually named 'flexible_variants' for the to-many
- * property access to resolve to the relation Collection.
- *
- * Both methods return the same relation — ApiPlatform's Eloquent metadata
- * factory deduplicates them into a single 'flexible_variants' property name.
- */
- public function flexible_variants(): HasMany // phpcs:ignore
- {
- return $this->flexibleVariants();
- }
- /**
- * Mirror the camelCase-loaded relation into the snake_case slot so that
- * $product->flexible_variants does not trigger a second DB query when
- * $with = ['flexibleVariants'] has already eager-loaded the variants.
- */
- public function getRelationValue($key)
- {
- if ($key === 'flexible_variants'
- && ! $this->relationLoaded('flexible_variants')
- && $this->relationLoaded('flexibleVariants')
- ) {
- $collection = $this->getRelation('flexibleVariants');
- $this->setRelation('flexible_variants', $collection);
- if ($collection->isNotEmpty() && ! $collection->first()->relationLoaded('variant_images')) {
- $collection->loadMissing(['variant_images', 'values.option','price_indices']);
- }
- return $collection;
- }
- return parent::getRelationValue($key);
- }
- /**
- * The videos that belong to the product.
- */
- public function videos(): HasMany
- {
- return $this->hasMany(ProductVideo::class, 'product_id')
- ->orderBy('position');
- }
- #[ApiProperty(
- writable: false,
- readable: true,
- required: false
- )]
- #[Groups(['mutation'])]
- public function getVideos()
- {
- return $this->videos;
- }
- public function setVideos($value): void
- {
- $this->videos = $value;
- }
- /**
- * The images that belong to the product.
- */
- public function base_image_url(): HasMany
- {
- return $this->hasMany(ProductImage::class, 'product_id')
- ->orderBy('position');
- }
- #[ApiProperty(
- writable: false,
- readable: true,
- required: true,
- readableLink: true
- )]
- public function getBase_image_url(): ProductImage
- {
- return $this->base_image_url;
- }
- public function channels(): BelongsToMany
- {
- return $this->belongsToMany(Channel::class, 'product_channels', 'product_id', 'channel_id');
- }
- #[ApiProperty(
- writable: true,
- readable: true,
- required: false
- )]
- #[Groups(['mutation'])]
- public function getChannels()
- {
- return $this->channels;
- }
- public function setChannels($value): void
- {
- $this->channels = $value;
- }
- /**
- * Get configurable product option index attribute.
- *
- * For configurable products, returns an index mapping variant IDs to their option values by attribute code.
- * Format: JSON string like { "588": { "color": 1, "size": 6 }, "589": { "color": 2, "size": 6 }, ... }
- *
- * This allows headless developers to identify which variant matches selected options.
- * Similar to Shop package's ConfigurableOption helper.
- */
- public function getIndexAttribute(): string
- {
- return $this->getCombinationsAttribute();
- }
- #[ApiProperty(deprecationReason: "Use the VariantAttributeMap property instead",writable: false, readable: true, required: false)]
- public function getIndex(): ?string
- {
- $indexJson = $this->getIndexAttribute();
- return $indexJson !== '{}' ? $indexJson : null;
- }
- public function getCombinationsAttribute(): string
- {
- if ($this->type !== 'configurable') {
- return '{}';
- }
- $index = [];
- if (! $this->relationLoaded('super_attributes')) {
- $this->load('super_attributes');
- }
- if (! $this->relationLoaded('variants')) {
- $this->load([
- 'variants' => function ($query) {
- $query->with(['attribute_values.attribute']);
- }
- ]);
- }
- $superAttributeIds = $this->super_attributes->pluck('id')->toArray();
- $attributeCodeMap = $this->super_attributes->pluck('code', 'id')->toArray();
- if (empty($superAttributeIds)) {
- return '{}';
- }
- foreach ($this->variants as $variant) {
- if (! isset($index[$variant->id])) {
- $index[$variant->id] = [];
- }
- // Load variant's attribute values if needed
- if (! $variant->relationLoaded('attribute_values')) {
- $variant->load('attribute_values.attribute');
- }
- // Get the attribute value for each super attribute
- foreach ($variant->attribute_values as $attrValue) {
- // Only include super attributes (configurable attributes)
- if (in_array($attrValue->attribute_id, $superAttributeIds)) {
- $attributeCode = $attributeCodeMap[$attrValue->attribute_id] ?? null;
- if ($attributeCode) {
- $index[$variant->id][$attributeCode] = $attrValue->value;
- }
- }
- }
- }
- return json_encode($index);
- }
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getCombinations(): ?string
- {
- $indexJson = $this->getCombinationsAttribute();
- return $indexJson !== '{}' ? $indexJson : null;
- }
- public function getSuperAttributeOptionsAttribute(): string
- {
- if ($this->type !== 'configurable') {
- return '{}';
- }
- // Ensure relations are loaded
- if (! $this->relationLoaded('super_attributes')) {
- $this->load('super_attributes');
- }
- if (! $this->relationLoaded('variants')) {
- $this->load([
- 'variants' => function ($query) {
- $query->with(['attribute_values.attribute.options']);
- }
- ]);
- }
- // Step 1: Collect used option IDs per attribute
- $usedOptions = [];
- foreach ($this->variants as $variant) {
- foreach ($variant->attribute_values as $attrValue) {
- $usedOptions[$attrValue->attribute_id][] = $attrValue->value;
- }
- }
- // Deduplicate
- foreach ($usedOptions as $attrId => $values) {
- $usedOptions[$attrId] = array_unique($values);
- }
- // Step 2: Build response
- $result = [];
- foreach ($this->super_attributes as $attribute) {
- $options = [];
- foreach ($attribute->options as $option) {
- if (in_array($option->id, $usedOptions[$attribute->id] ?? [])) {
- $options[] = [
- 'id' => $option->id,
- 'label' => $option->admin_name,
- ];
- }
- }
- if (! empty($options)) {
- $result[] = [
- 'id' => $attribute->id,
- 'code' => $attribute->code,
- 'label' => $attribute->admin_name,
- 'options' => $options,
- ];
- }
- }
- return json_encode($result);
- }
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getSuper_attribute_options(): ?string
- {
- $indexJson = $this->getSuperAttributeOptionsAttribute();
- return $indexJson !== '{}' ? $indexJson : null;
- }
-
-
- public function getSkuAttribute(): ?string
- {
- return $this->getSystemAttributeValue('sku');
- }
- #[ApiProperty(
- writable: true,
- readable: true
- )]
- #[Groups(['mutation'])]
- public function getSku(): ?string
- {
- return $this->getSkuAttribute();
- }
- public function setSku(?string $value): void
- {
- $this->setSystemAttributeValue('sku', $value);
- }
- #[ApiProperty(
- writable: true,
- readable: true,
- required: true
- )]
- #[Groups(['mutation'])]
- public function getType(): ?string
- {
- return $this->type;
- }
- public function setType(?string $value): void
- {
- $this->type = $value;
- }
- #[ApiProperty(
- writable: true,
- readable: true,
- required: true
- )]
- #[Groups(['mutation'])]
- public function getAttribute_family(): ?AttributeFamily
- {
- return $this->attribute_family;
- }
- public function setAttribute_family(?AttributeFamily $value): void
- {
- $this->attribute_family = $value;
- }
- /**
- * Get attribute family relationship
- * Override to return BagistoApi AttributeFamily model
- */
- public function attribute_family(): BelongsTo
- {
- return $this->belongsTo(AttributeFamily::class, 'attribute_family_id');
- }
- #[ApiProperty(
- writable: true,
- readable: true,
- required: true
- )]
- public function getBookingProductsAttributes()
- {
- return $this->getBookingProducts();
- }
- /**
- * Get booking products.
- */
- public function getBookingProducts()
- {
- return $this->booking_products;
- }
- /**
- * Set booking products.
- */
- public function setBookingProducts($value): void
- {
- $this->booking_products = $value;
- }
- /**
- * Get the booking products relationship.
- */
- public function booking_products(): HasMany
- {
- return $this->hasMany(BookingProduct::class, 'product_id');
- }
- /**
- * Get bundle options.
- */
- #[ApiProperty(
- writable: false,
- readable: true,
- required: false
- )]
- #[Groups(['mutation'])]
- public function getBundleOptions()
- {
- return $this->bundle_options;
- }
- public function setBundleOptions($value): void
- {
- $this->bundle_options = $value;
- }
- /**
- * Get the bundle options relationship.
- */
- public function bundle_options(): HasMany
- {
- return $this->hasMany(ProductBundleOption::class);
- }
- public function grouped_products(): HasMany
- {
- return $this->hasMany(ProductGroupedProduct::class, 'product_id');
- }
- public function downloadable_links(): HasMany
- {
- return $this->hasMany(ProductDownloadableLink::class, 'product_id');
- }
- public function downloadable_samples(): HasMany
- {
- return $this->hasMany(ProductDownloadableSample::class, 'product_id');
- }
- /**
- * Get downloadable samples.
- */
- #[ApiProperty(
- writable: false,
- readable: true,
- required: false
- )]
- #[Groups(['mutation'])]
- public function getDownloadableSamples()
- {
- return $this->downloadable_samples;
- }
- public function setDownloadableSamples($value): void
- {
- $this->downloadable_samples = $value;
- }
- /**
- * The customizable options that belong to the product.
- */
- public function customizable_options(): HasMany
- {
- return $this->hasMany(ProductCustomizableOption::class, 'product_id')
- ->orderBy('sort_order');
- }
- /**
- * Get customizable options.
- */
- #[ApiProperty(
- writable: false,
- readable: true,
- required: false
- )]
- #[Groups(['mutation'])]
- public function getCustomizable_options()
- {
- // Eager load prices to ensure they're properly constrained
- return $this->customizable_options()
- ->with('customizable_option_prices')
- ->get();
- }
- public function setCustomizable_options($value): void
- {
- $this->customizable_options = $value;
- }
- /**
- * Get name attribute.
- */
- public function getNameAttribute(): ?string
- {
- return $this->getSystemAttributeValue('name');
- }
- #[ApiProperty(
- writable: true,
- readable: true,
- required: false
- )]
- public function getName(): ?string
- {
- return $this->getNameAttribute();
- }
- public function setName(?string $value): void
- {
- $this->setSystemAttributeValue('name', $value);
- }
- // ========================================
- // URL Key (text, per locale) - Only for update
- // ========================================
- public function getUrlKeyAttribute(): ?string
- {
- return $this->getSystemAttributeValue('url_key');
- }
- #[ApiProperty(
- writable: true,
- readable: true,
- required: false
- )]
- public function getUrl_key(): ?string
- {
- return $this->getUrlKeyAttribute();
- }
- public function setUrl_key(?string $value): void
- {
- $this->setSystemAttributeValue('url_key', $value);
- }
- // ========================================
- // Status (boolean, per channel)
- // ========================================
- public function getStatusAttribute(): ?bool
- {
- return $this->getSystemAttributeValue('status');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getStatus(): ?bool
- {
- return $this->getStatusAttribute();
- }
- public function setStatus(?bool $value): void
- {
- $this->setSystemAttributeValue('status', $value);
- }
- // ========================================
- // Description (textarea, per locale)
- // ========================================
- public function getDescriptionAttribute(): ?string
- {
- return $this->getSystemAttributeValue('description');
- }
- #[ApiProperty(writable: true, readable: true, required: false)]
- public function getDescription(): ?string
- {
- return $this->getDescriptionAttribute();
- }
- public function setDescription(?string $value): void
- {
- $this->setSystemAttributeValue('description', $value);
- }
- /**
- * Laravel accessor for description_html appended attribute
- */
- public function getDescriptionHtmlAttribute(): ?string
- {
- return $this->getDescriptionAttribute();
- }
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getDescription_html(): ?string
- {
- return $this->getDescription_htmlAttribute();
- }
- public function getShortDescriptionAttribute(): ?string
- {
- return $this->getSystemAttributeValue('short_description');
- }
- #[ApiProperty(
- writable: true,
- readable: true,
- required: false,
- schema: ['type' => 'string', 'nullable' => true],
- openapiContext: ['nullable' => true],
- jsonSchemaContext: ['type' => 'string', 'nullable' => true]
- )]
- public function getShort_description(): ?string
- {
- return $this->getShort_descriptionAttribute();
- }
- public function setShort_description(?string $value): void
- {
- $this->setSystemAttributeValue('short_description', $value);
- }
- public function getPriceAttribute(): ?float
- {
- return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price')));
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getPrice(): ?float
- {
- return $this->getPriceAttribute();
- }
- public function setPrice(?float $value): void
- {
- $this->setSystemAttributeValue('price', $value);
- }
- public function getSpecialPriceAttribute(): ?float
- {
- $value = floatval($this->getSystemAttributeValue('special_price'));
- return $value ? (float) core()->convertPrice($value) : null;
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getSpecial_price(): ?float
- {
- return $this->getSpecialPriceAttribute();
- }
- public function setSpecial_price(?float $value): void
- {
- $this->setSystemAttributeValue('special_price', $value);
- }
- public function getWeightAttribute(): ?string
- {
- return $this->getSystemAttributeValue('weight');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getWeight(): ?string
- {
- return $this->getWeightAttribute();
- }
- public function setWeight(?string $value): void
- {
- $this->setSystemAttributeValue('weight', $value);
- }
- public function getProductNumberAttribute(): ?string
- {
- return $this->getSystemAttributeValue('product_number');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getProduct_number(): ?string
- {
- return $this->getProductNumberAttribute();
- }
- public function setProduct_number(?string $value): void
- {
- $this->setSystemAttributeValue('product_number', $value);
- }
- public function getNewAttribute(): ?bool
- {
- return $this->getSystemAttributeValue('new');
- }
- #[ApiProperty(
- writable: true,
- readable: true,
- required: false,
- schema: ['type' => 'boolean', 'nullable' => true],
- )]
- public function getNew(): bool
- {
- return $this->getNewAttribute();
- }
- public function setNew(bool $value): void
- {
- $this->setSystemAttributeValue('new', $value);
- }
- public function getFeaturedAttribute(): ?bool
- {
- return $this->getSystemAttributeValue('featured');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getFeatured(): ?bool
- {
- return $this->getFeaturedAttribute();
- }
- public function setFeatured(?bool $value): void
- {
- $this->setSystemAttributeValue('featured', $value);
- }
- // ========================================
- // Visible Individually (boolean)
- // ========================================
- public function getVisibleIndividuallyAttribute(): ?bool
- {
- return $this->getSystemAttributeValue('visible_individually');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getVisible_individually(): ?bool
- {
- return $this->getVisibleIndividuallyAttribute();
- }
- public function setVisible_individually(?bool $value): void
- {
- $this->setSystemAttributeValue('visible_individually', $value);
- }
- // ========================================
- // Guest Checkout (boolean)
- // ========================================
- public function getGuestCheckoutAttribute(): ?bool
- {
- return $this->getSystemAttributeValue('guest_checkout');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getGuest_checkout(): ?bool
- {
- return $this->getGuestCheckoutAttribute();
- }
- public function setGuest_checkout(?bool $value): void
- {
- $this->setSystemAttributeValue('guest_checkout', $value);
- }
- // ========================================
- // Manage Stock (boolean, per channel)
- // ========================================
- public function getManageStockAttribute(): ?bool
- {
- return $this->getSystemAttributeValue('manage_stock');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getManage_stock(): ?bool
- {
- return $this->getManageStockAttribute();
- }
- public function setManage_stock(?bool $value): void
- {
- $this->setSystemAttributeValue('manage_stock', $value);
- }
- // ========================================
- // Meta Title (textarea, per locale)
- // ========================================
- public function getMetaTitleAttribute(): ?string
- {
- return $this->getSystemAttributeValue('meta_title');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getMeta_title(): ?string
- {
- return $this->getMetaTitleAttribute();
- }
- public function setMeta_title(?string $value): void
- {
- $this->setSystemAttributeValue('meta_title', $value);
- }
- // ========================================
- // Meta Keywords (textarea, per locale)
- // ========================================
- public function getMetaKeywordsAttribute(): ?string
- {
- return $this->getSystemAttributeValue('meta_keywords');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getMeta_keywords(): ?string
- {
- return $this->getMetaKeywordsAttribute();
- }
- public function setMeta_keywords(?string $value): void
- {
- $this->setSystemAttributeValue('meta_keywords', $value);
- }
- // ========================================
- // Tax Category ID (select, per channel)
- // ========================================
- public function getTaxCategoryIdAttribute(): ?int
- {
- return (int) $this->getSystemAttributeValue('tax_category_id');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getTax_category_id(): ?int
- {
- return $this->getTaxCategoryIdAttribute();
- }
- public function setTax_category_id(?int $value): void
- {
- $this->setSystemAttributeValue('tax_category_id', $value);
- }
- // ========================================
- // Special Price From (date, per channel)
- // ========================================
- public function getSpecialPriceFromAttribute(): ?string
- {
- return $this->getSystemAttributeValue('special_price_from');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getSpecial_price_from(): ?string
- {
- return $this->getSpecialPriceFromAttribute();
- }
- public function setSpecial_price_from(?string $value): void
- {
- $this->setSystemAttributeValue('special_price_from', $value);
- }
- // ========================================
- // Special Price To (date, per channel)
- // ========================================
- public function getSpecialPriceToAttribute(): ?string
- {
- return $this->getSystemAttributeValue('special_price_to');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getSpecial_price_to(): ?string
- {
- return $this->getSpecialPriceToAttribute();
- }
- public function setSpecial_price_to(?string $value): void
- {
- $this->setSystemAttributeValue('special_price_to', $value);
- }
- // ========================================
- // Cost (price) - User-defined
- // ========================================
- public function getCostAttribute()
- {
- return floatval($this->getSystemAttributeValue('cost'));
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getCost(): ?float
- {
- return $this->getCostAttribute();
- }
- public function setCost(?float $value): void
- {
- $this->setSystemAttributeValue('cost', $value);
- }
- // ========================================
- // Meta Description (textarea, per locale) - User-defined
- // ========================================
- public function getMetaDescriptionAttribute(): ?string
- {
- return $this->getSystemAttributeValue('meta_description');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getMeta_description(): ?string
- {
- return $this->getMetaDescriptionAttribute();
- }
- public function setMeta_description(?string $value): void
- {
- $this->setSystemAttributeValue('meta_description', $value);
- }
- // ========================================
- // Length (text) - User-defined
- // ========================================
- public function getLengthAttribute(): ?string
- {
- return $this->getSystemAttributeValue('length');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getLength(): ?string
- {
- return $this->getLengthAttribute();
- }
- public function setLength(?string $value): void
- {
- $this->setSystemAttributeValue('length', $value);
- }
- // ========================================
- // Width (text) - User-defined
- // ========================================
- public function getWidthAttribute(): ?string
- {
- return $this->getSystemAttributeValue('width');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getWidth(): ?string
- {
- return $this->getWidthAttribute();
- }
- public function setWidth(?string $value): void
- {
- $this->setSystemAttributeValue('width', $value);
- }
- // ========================================
- // Height (text) - User-defined
- // ========================================
- public function getHeightAttribute(): ?string
- {
- return $this->getSystemAttributeValue('height');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getHeight(): ?string
- {
- return $this->getHeightAttribute();
- }
- public function setHeight(?string $value): void
- {
- $this->setSystemAttributeValue('height', $value);
- }
- // ========================================
- // Color (select) - User-defined
- // ========================================
- public function getColorAttribute()
- {
- return $this->getSystemAttributeValue('color');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getColor(): ?int
- {
- return $this->getColorAttribute();
- }
- public function setColor(?int $value): void
- {
- $this->setSystemAttributeValue('color', $value);
- }
- // ========================================
- // Size (select) - User-defined
- // ========================================
- public function getSizeAttribute()
- {
- $sizeValue = $this->getSystemAttributeValue('size');
- return is_array($sizeValue) ? implode(',', $sizeValue) : $sizeValue;
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getSize(): ?int
- {
- return $this->getSizeAttribute();
- }
- public function setSize(?int $value): void
- {
- $this->setSystemAttributeValue('size', $value);
- }
- // ========================================
- // Brand (select) - User-defined
- // ========================================
- public function getBrandAttribute()
- {
- return $this->getSystemAttributeValue('brand');
- }
- #[ApiProperty(writable: true, readable: true)]
- public function getBrand(): ?int
- {
- return $this->getBrandAttribute();
- }
- public function setBrand(?int $value): void
- {
- $this->setSystemAttributeValue('brand', $value);
- }
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getReviews()
- {
- return function ($source, array $args = [], $context = null) {
- $relation = $this->reviews();
- /** Only return approved reviews unless a specific status is requested */
- $relation = $relation->where('status', $args['status'] ?? 'approved');
- if (isset($args['first']) && is_numeric($args['first'])) {
- $relation = $relation->limit((int) $args['first']);
- }
- if (empty($args) && $this->relationLoaded('reviews')) {
- return $this->reviews->where('status', 'approved')->values();
- }
- return $relation->get();
- };
- }
- /**
- * Get the product reviews that owns the product.
- */
- public function reviews(): HasMany
- {
- return $this->hasMany(ProductReview::class);
- }
- // ========================================
- // Helper Methods
- // ========================================
- /**
- * Cache for attribute values to avoid repeated lookups
- */
- protected array $attributeValueCache = [];
- /**
- * Get a system attribute value from product_attribute_values
- * This reads from the database when querying
- * OPTIMIZED: Uses memoization to cache attribute values within the same request
- */
- protected function getSystemAttributeValue(string $attributeCode): mixed
- {
- // Check cache first
- if (array_key_exists($attributeCode, $this->attributeValueCache)) {
- return $this->attributeValueCache[$attributeCode];
- }
- // If value was set via setter (during input), return it from temporary storage
- $tempKey = "_temp_{$attributeCode}";
- if (isset($this->attributes[$tempKey])) {
- return $this->attributeValueCache[$attributeCode] = $this->attributes[$tempKey];
- }
- // Otherwise, read from database via relationship
- if (! $this->relationLoaded('attribute_values')) {
- $this->load('attribute_values');
- }
- $attrConfig = static::$systemAttributes[$attributeCode] ?? null;
- if (! $attrConfig) {
- return $this->attributeValueCache[$attributeCode] = '';
- }
- $currentLocale = $this->locale ?? app()->getLocale();
- $currentChannel = $this->channel ?? (core()->getCurrentChannel()->code ?? 'default');
- $attributeValue = null;
-
- $localeVariants = [];
- if (! empty($currentLocale)) {
- $localeVariants[] = $currentLocale;
- if (str_contains($currentLocale, '_') || str_contains($currentLocale, '-')) {
- $localeParts = preg_split('/[_-]/', $currentLocale);
- if (! empty($localeParts[0])) {
- $localeVariants[] = $localeParts[0];
- }
- }
- }
- // Fallback to the channel's default locale when the requested locale has no translation
- $defaultLocale = core()->getCurrentChannel()->default_locale?->code;
- if ($defaultLocale && ! in_array($defaultLocale, $localeVariants)) {
- $localeVariants[] = $defaultLocale;
- }
- $localeVariants[] = null;
-
- $channelVariants = [$currentChannel, null];
- foreach ($localeVariants as $localeVariant) {
- foreach ($channelVariants as $channelVariant) {
- $query = $this->attribute_values->where('attribute_id', $attrConfig['id']);
- if ($localeVariant === null) {
- $query = $query->whereNull('locale');
- } else {
- $query = $query->where('locale', $localeVariant);
- }
- if ($channelVariant === null) {
- $query = $query->whereNull('channel');
- } else {
- $query = $query->where('channel', $channelVariant);
- }
- $attributeValue = $query->first();
- if ($attributeValue) {
- break 2;
- }
- }
- }
- if ($attributeValue && $attributeValue?->integer_value && in_array($attributeValue?->attribute?->type, ['select', 'multiselect', 'checkbox'])) {
- $attributeValue->setValue($attributeValue->attribute->options()->where('id', $attributeValue->value)->first()?->label);
- }
-
- return $this->attributeValueCache[$attributeCode] = ($attributeValue ? $attributeValue->value : '');
- }
- /**
- * Set a system attribute value (will be processed by ProductProcessor)
- * This stores in temporary attributes array for processing later
- */
- protected function setSystemAttributeValue(string $attributeCode, mixed $value): void
- {
- $tempKey = "_temp_{$attributeCode}";
- $this->attributes[$tempKey] = $value;
- }
- public function related_products(): BelongsToMany
- {
- return $this->belongsToMany(static::class, 'product_relations', 'parent_id', 'child_id');
- }
- #[ApiProperty(writable: true, readable: true, required: false)]
- public function getRelatedProducts()
- {
- return function ($source, array $args = [], $context = null) {
-
- $relation = $source->related_products();
-
- $total = $relation->count();
- $limit = $args['first'] ?? $args['last'] ?? 30;
- $items = $relation->limit($limit)->get();
-
- return new \Illuminate\Pagination\LengthAwarePaginator(
- $items,
- $total,
- $limit,
- 1,
- ['path' => '/']
- );
- };
- }
- public function up_sells(): BelongsToMany
- {
- return $this->belongsToMany(static::class, 'product_up_sells', 'parent_id', 'child_id');
- }
- #[ApiProperty(writable: true, readable: true, required: false)]
- public function getUpSells()
- {
- // Return a Closure so ResourceFieldResolver invokes it with ($source, $args, $context)
- return function ($source, array $args = [], $context = null) {
- $relation = $source->up_sells();
- // Get total count before applying limit
- $total = $relation->count();
- // Apply first/last pagination if provided
- $limit = $args['first'] ?? $args['last'] ?? 30;
- $items = $relation->limit($limit)->get();
- // Return a LengthAwarePaginator so ApiPlatform can compute totalCount
- return new \Illuminate\Pagination\LengthAwarePaginator(
- $items,
- $total,
- $limit,
- 1,
- ['path' => '/']
- );
- };
- }
- public function cross_sells(): BelongsToMany
- {
- return $this->belongsToMany(static::class, 'product_cross_sells', 'parent_id', 'child_id');
- }
- #[ApiProperty(writable: true, readable: true, required: false)]
- public function getCrossSells()
- {
- // Return a Closure so ResourceFieldResolver invokes it with ($source, $args, $context)
- return function ($source, array $args = [], $context = null) {
- $relation = $source->cross_sells();
- // Get total count before applying limit
- $total = $relation->count();
- // Apply first/last pagination if provided
- $limit = $args['first'] ?? $args['last'] ?? 30;
- $items = $relation->limit($limit)->get();
- // Return a LengthAwarePaginator so ApiPlatform can compute totalCount
- return new \Illuminate\Pagination\LengthAwarePaginator(
- $items,
- $total,
- $limit,
- 1,
- ['path' => '/']
- );
- };
- }
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getProductPrices()
- {
- return $this->product_prices;
- }
- // ========================================
- // Minimum and Maximum Price (computed)
- // ========================================
- /**
- * Laravel accessor for minimum_price attribute.
- * Get product minimum price based on price index.
- * Falls back to base price if no price index is available.
- */
- public function getMinimumPriceAttribute(): float
- {
- try {
- // Load price indices if not already loaded
- if (! $this->relationLoaded('price_indices')) {
- $this->load('price_indices');
- }
- // Get current channel and customer group
- $currentChannel = core()->getCurrentChannel();
- $customerGroup = resolve('Webkul\Customer\Repositories\CustomerRepository')->getCurrentGroup();
- if (! $currentChannel || ! $customerGroup) {
- return floatval($this->price ?? 0);
- }
- // Get price index for current channel and customer group
- $priceIndex = $this->price_indices
- ->where('channel_id', $currentChannel->id)
- ->where('customer_group_id', $customerGroup->id)
- ->first();
- if ($priceIndex) {
- return (float) core()->convertPrice(floatval($priceIndex->min_price));
- }
- // Fallback to base price
- return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
- } catch (\Exception $e) {
- // If any error occurs, return base price
- return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
- }
- }
- /**
- * Get product minimum price for BagistoApi API.
- * Exposed to BagistoApi schema via ApiProperty attribute.
- */
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getMinimum_price(): float
- {
- return $this->getMinimumPriceAttribute();
- }
- /**
- * Laravel accessor for maximum_price attribute.
- * Get product maximum price based on price index.
- * Falls back to base price if no price index is available.
- */
- public function getMaximumPriceAttribute(): float
- {
- try {
- // Load price indices if not already loaded
- if (! $this->relationLoaded('price_indices')) {
- $this->load('price_indices');
- }
- // Get current channel and customer group
- $currentChannel = core()->getCurrentChannel();
- $customerGroup = resolve('Webkul\Customer\Repositories\CustomerRepository')->getCurrentGroup();
- if (! $currentChannel || ! $customerGroup) {
- return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
- }
- // Get price index for current channel and customer group
- $priceIndex = $this->price_indices
- ->where('channel_id', $currentChannel->id)
- ->where('customer_group_id', $customerGroup->id)
- ->first();
- if ($priceIndex) {
- return (float) core()->convertPrice(floatval($priceIndex->max_price));
- }
- // Fallback to base price
- return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
- } catch (\Exception $e) {
- // If any error occurs, return base price
- return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
- }
- }
- /**
- * Get product maximum price for BagistoApi API.
- * Exposed to BagistoApi schema via ApiProperty attribute.
- */
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getMaximum_price(): float
- {
- return $this->getMaximumPriceAttribute();
- }
- /**
- * Laravel accessor for regular_minimum_price attribute.
- * Get product regular minimum price based on price index.
- * Falls back to base price if no price index is available.
- */
- public function getRegularMinimumPriceAttribute(): float
- {
- try {
- // Load price indices if not already loaded
- if (! $this->relationLoaded('price_indices')) {
- $this->load('price_indices');
- }
- // Get current channel and customer group
- $currentChannel = core()->getCurrentChannel();
- $customerGroup = resolve('Webkul\Customer\Repositories\CustomerRepository')->getCurrentGroup();
- if (! $currentChannel || ! $customerGroup) {
- return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
- }
- // Get price index for current channel and customer group
- $priceIndex = $this->price_indices
- ->where('channel_id', $currentChannel->id)
- ->where('customer_group_id', $customerGroup->id)
- ->first();
- if ($priceIndex) {
- return (float) core()->convertPrice(floatval($priceIndex->regular_min_price));
- }
- // Fallback to base price
- return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
- } catch (\Exception $e) {
- // If any error occurs, return base price
- return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
- }
- }
- /**
- * Get product regular minimum price for BagistoApi API.
- * Exposed to BagistoApi schema via ApiProperty attribute.
- */
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getRegular_minimum_price(): float
- {
- return $this->getRegularMinimumPriceAttribute();
- }
- /**
- * Laravel accessor for regular_maximum_price attribute.
- * Get product regular maximum price based on price index.
- * Falls back to base price if no price index is available.
- */
- public function getRegularMaximumPriceAttribute(): float
- {
- try {
- // Load price indices if not already loaded
- if (! $this->relationLoaded('price_indices')) {
- $this->load('price_indices');
- }
- // Get current channel and customer group
- $currentChannel = core()->getCurrentChannel();
- $customerGroup = resolve('Webkul\Customer\Repositories\CustomerRepository')->getCurrentGroup();
- if (! $currentChannel || ! $customerGroup) {
- return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
- }
- // Get price index for current channel and customer group
- $priceIndex = $this->price_indices
- ->where('channel_id', $currentChannel->id)
- ->where('customer_group_id', $customerGroup->id)
- ->first();
- if ($priceIndex) {
- return (float) core()->convertPrice(floatval($priceIndex->regular_max_price));
- }
- // Fallback to base price
- return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
- } catch (\Exception $e) {
- // If any error occurs, return base price
- return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
- }
- }
- /**
- * Get product regular maximum price for BagistoApi API.
- * Exposed to BagistoApi schema via ApiProperty attribute.
- */
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getRegular_maximum_price(): float
- {
- return $this->getRegularMaximumPriceAttribute();
- }
- // ─── Formatted Price Accessors ──────────────────────────────────────
- public function getFormattedPriceAttribute(): ?string
- {
- $price = $this->getPriceAttribute();
- return $price !== null ? core()->formatPrice($price) : null;
- }
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getFormatted_price(): ?string
- {
- return $this->getFormattedPriceAttribute();
- }
- public function getFormattedSpecialPriceAttribute(): ?string
- {
- $specialPrice = $this->getSpecialPriceAttribute();
- return $specialPrice ? core()->formatPrice($specialPrice) : null;
- }
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getFormatted_special_price(): ?string
- {
- return $this->getFormattedSpecialPriceAttribute();
- }
- public function getFormattedMinimumPriceAttribute(): ?string
- {
- return core()->formatPrice($this->getMinimumPriceAttribute());
- }
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getFormatted_minimum_price(): ?string
- {
- return $this->getFormattedMinimumPriceAttribute();
- }
- public function getFormattedMaximumPriceAttribute(): ?string
- {
- return core()->formatPrice($this->getMaximumPriceAttribute());
- }
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getFormatted_maximum_price(): ?string
- {
- return $this->getFormattedMaximumPriceAttribute();
- }
- public function getFormattedRegularMinimumPriceAttribute(): ?string
- {
- return core()->formatPrice($this->getRegularMinimumPriceAttribute());
- }
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getFormatted_regular_minimum_price(): ?string
- {
- return $this->getFormattedRegularMinimumPriceAttribute();
- }
- public function getFormattedRegularMaximumPriceAttribute(): ?string
- {
- return core()->formatPrice($this->getRegularMaximumPriceAttribute());
- }
- #[ApiProperty(writable: false, readable: true, required: false)]
- public function getFormatted_regular_maximum_price(): ?string
- {
- return $this->getFormattedRegularMaximumPriceAttribute();
- }
- }
|