Source of file ProductGroup.php
Size: 25,736 Bytes - Last Modified: 2021-12-23T10:39:35+00:00
/var/www/docs.ssmods.com/process/src/src/Pages/ProductGroup.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785 | <?php namespace Sunnysideup\Ecommerce\Pages; use Page; use SilverStripe\Assets\Image; use SilverStripe\Control\Controller; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter; use SilverStripe\Forms\HeaderField; use SilverStripe\Forms\NumericField; use SilverStripe\Forms\ReadonlyField; use SilverStripe\Forms\Tab; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBField; use SilverStripe\Security\Permission; use Sunnysideup\Ecommerce\Api\ArrayMethods; use Sunnysideup\Ecommerce\Api\ClassHelpers; use Sunnysideup\Ecommerce\Api\EcommerceCache; use Sunnysideup\Ecommerce\Cms\ProductsAndGroupsModelAdmin; use Sunnysideup\Ecommerce\Config\EcommerceConfig; use Sunnysideup\Ecommerce\Config\EcommerceConfigClassNames; use Sunnysideup\Ecommerce\Forms\Fields\ProductProductImageUploadField; use Sunnysideup\Ecommerce\Forms\Gridfield\Configs\GridFieldBasicPageRelationConfig; use Sunnysideup\Ecommerce\Model\Extensions\EcommerceRole; use Sunnysideup\Ecommerce\Model\Search\ProductGroupSearchTable; use Sunnysideup\Ecommerce\ProductsAndGroups\Applyers\BaseApplyer; use Sunnysideup\Ecommerce\ProductsAndGroups\Applyers\ProductSearchFilter; use Sunnysideup\Ecommerce\ProductsAndGroups\Builders\BaseProductList; use Sunnysideup\Ecommerce\ProductsAndGroups\ProductGroupSchema; use Sunnysideup\Vardump\Vardump; /** * Product Group is a 'holder' for Products within the CMS. * * @author: Nicolaas [at] Sunny Side Up .co.nz * @package: ecommerce * @subpackage: Pages */ class ProductGroup extends Page { protected $baseProductList; protected static $filterForCandidateCategoriesCache = []; /** * set by ID of RootGroup. * * @var array */ protected static $searchStringCache = []; protected static $parentGroupCache; protected static $parentPageCache = []; protected static $topParentGroupCache = []; protected static $getProductCountCache = []; /** * @var array */ protected static $recursiveValuesCache = []; private static $template_for_selection_of_products = ProductGroupSchema::class; /** * @var string */ private static $base_buyable_class = Product::class; private static $maximum_number_of_products_to_list = 999; private static $table_name = 'ProductGroup'; private static $db = [ 'NumberOfProductsPerPage' => 'Int', 'LevelOfProductsToShow' => 'Int', 'DefaultSortOrder' => 'Varchar(20)', 'DefaultFilter' => 'Varchar(20)', 'DisplayStyle' => 'Varchar(20)', ]; private static $has_one = [ 'Image' => Image::class, ]; private static $owns = [ 'Image', ]; private static $belongs_many_many = [ 'AlsoShowProducts' => Product::class, ]; private static $defaults = [ 'DefaultSortOrder' => BaseApplyer::DEFAULT_NAME, 'DefaultFilter' => BaseApplyer::DEFAULT_NAME, 'DisplayStyle' => BaseApplyer::DEFAULT_NAME, 'LevelOfProductsToShow' => 99, ]; private static $indexes = [ 'LevelOfProductsToShow' => true, 'DefaultSortOrder' => true, 'DefaultFilter' => true, 'DisplayStyle' => true, ]; private static $summary_fields = [ 'Image.CMSThumbnail' => 'Image', 'Title' => 'Category', 'NumberOfProducts' => 'Direct Products', 'AlsoShowProducts.Count' => 'Also Show Products', 'Children.Count' => 'Child Categories', ]; private static $searchable_fields = [ 'ShowInMenus' => 'ExactMatchFilter', 'ShowInSearch' => 'ExactMatchFilter', ]; private static $casting = [ 'NumberOfProducts' => 'Int', ]; private static $default_child = Product::class; private static $icon = 'sunnysideup/ecommerce:client/images/icons/productgroup-file.gif'; private static $singular_name = 'Product Category'; private static $plural_name = 'Product Categories'; private static $description = 'A page the shows a bunch of products, based on your selection. By default it shows products linked to it (children)'; private static $count = 0; public function i18n_singular_name() { return _t('ProductGroup.SINGULARNAME', 'Product Category'); } public function i18n_plural_name() { return _t('ProductGroup.PLURALNAME', 'Product Categories'); } public function canCreate($member = null, $context = []) { $extended = $this->extendedCan(__FUNCTION__, $member); if (null !== $extended) { return $extended; } if (Permission::checkMember($member, Config::inst()->get(EcommerceRole::class, 'admin_permission_code'))) { return true; } return parent::canCreate($member, $context); } /** * Shop Admins can edit. * * @param \SilverStripe\Security\Member $member * @param mixed $context * * @return bool */ public function canEdit($member = null, $context = []) { $extended = $this->extendedCan(__FUNCTION__, $member); if (null !== $extended) { return $extended; } if (Permission::checkMember($member, Config::inst()->get(EcommerceRole::class, 'admin_permission_code'))) { return true; } return parent::canEdit($member); } /** * Standard SS method. * * @param \SilverStripe\Security\Member $member * * @return bool */ public function canDelete($member = null) { if (is_a(Controller::curr(), EcommerceConfigClassNames::getName(ProductsAndGroupsModelAdmin::class))) { return false; } $extended = $this->extendedCan(__FUNCTION__, $member); if (null !== $extended) { return $extended; } return $this->canEdit($member); } /** * Standard SS method. * * @param \SilverStripe\Security\Member $member * * @return bool */ public function canPublish($member = null) { return parent::canEdit($member); } public function getCMSFields() { $fields = parent::getCMSFields(); $fields->addFieldToTab( 'Root.Images', ProductProductImageUploadField::create('Image', _t('Product.IMAGE', 'Product Group Image')) ); $calculatedNumberOfProductsPerPage = $this->getProductsPerPage(); $numberOfProductsPerPageExplanation = $calculatedNumberOfProductsPerPage !== $this->NumberOfProductsPerPage ? _t('ProductGroup.CURRENTLVALUE', 'Current value: ') . $calculatedNumberOfProductsPerPage . ' ' . _t('ProductGroup.INHERITEDFROMPARENTSPAGE', ' (inherited from parent page because the current page is set to zero)') : ''; $fields->addFieldToTab( 'Root', Tab::create( 'ProductDisplay', _t('ProductGroup.DISPLAY', 'Display'), $productsToShowField = DropdownField::create('LevelOfProductsToShow', _t('ProductGroup.PRODUCTSTOSHOW', 'Products to show'), $this->getShowProductLevelsArray()), HeaderField::create('WhatProductsAreShown', _t('ProductGroup.WHATPRODUCTSSHOWN', _t('ProductGroup.OPTIONSSELECTEDBELOWAPPLYTOCHILDGROUPS', 'Inherited options'))), $numberOfProductsPerPageField = NumericField::create('NumberOfProductsPerPage', _t('ProductGroup.PRODUCTSPERPAGE', 'Number of products per page')) ) ); $numberOfProductsPerPageField->setDescription($numberOfProductsPerPageExplanation); if ($calculatedNumberOfProductsPerPage && ! $this->NumberOfProductsPerPage) { $this->NumberOfProductsPerPage = 0; $numberOfProductsPerPageField->setAttribute('placeholder', $calculatedNumberOfProductsPerPage); } $this->addDropDownForListConfig($fields, 'FILTER', _t('ProductGroup.DEFAULTFILTER', 'Default Filter')); $this->addDropDownForListConfig($fields, 'SORT', _t('ProductGroup.DEFAULTSORTORDER', 'Default Sort Order')); $this->addDropDownForListConfig($fields, 'DISPLAY', _t('ProductGroup.DEFAULTDISPLAYSTYLE', 'Default Display Style')); $config = EcommerceConfig::inst(); if ($config->ProductsAlsoInOtherGroups) { if (! ClassHelpers::check_for_instance_of($this, ProductGroupSearchPage::class, false)) { $fields->addFieldsToTab( 'Root.OtherProductsShown', [ HeaderField::create('ProductGroupsHeader', _t('ProductGroup.OTHERPRODUCTSTOSHOW', 'Other products to show ...')), $this->getProductGroupsTable(), ] ); } } $fields->addFieldsToTab( 'Root.Advanced', ReadonlyField::create( 'DebugLink', 'Debug Products and Links', DBField::create_field('HTMLText', '<a href="' . $this->Link() . '?showdebug=1">show debug information</a>') ) ); return $fields; } public function FilterForGroupSegment(): string { return $this->URLSegment . '.' . $this->ID; } /** * @EcommerCache Candidate */ public function getFilterForCandidateCategories(): DataList { if (! isset(self::$filterForCandidateCategoriesCache[$this->ID])) { self::$filterForCandidateCategoriesCache[$this->ID] = $this->getBaseProductList()->getFilterForCandidateCategories(); } return self::$filterForCandidateCategoriesCache[$this->ID]; } /** * used if you install lumberjack. */ public function getLumberjackTitle(): string { return _t('ProductGroup.BUYABLES', 'Products'); } public function requireDefaultRecords() { parent::requireDefaultRecords(); $urlSegments = ProductGroup::get()->column('URLSegment'); foreach ($urlSegments as $urlSegment) { $counts = array_count_values($urlSegments); $hasDuplicates = $counts[$urlSegment] > 1; if ($hasDuplicates) { DB::alteration_message('found duplicates for ' . $urlSegment, 'deleted'); $checkForDuplicatesURLSegments = ProductGroup::get() ->filter(['URLSegment' => $urlSegment]) ; if ($checkForDuplicatesURLSegments->exists()) { $count = 0; foreach ($checkForDuplicatesURLSegments as $productGroup) { if ($count > 0) { $oldURLSegment = $productGroup->URLSegment; DB::alteration_message(' ... Correcting URLSegment for ' . $productGroup->Title . ' with ID: ' . $productGroup->ID, 'deleted'); $productGroup->writeToStage('Stage'); $productGroup->publishRecursive(); $newURLSegment = $productGroup->URLSegment; DB::alteration_message(' ... .... from ' . $oldURLSegment . ' to ' . $newURLSegment, 'created'); } ++$count; } } } } } /** * returns the template for providing related groups and products. * * @return ProductGroupSchema */ public function getProductGroupSchema() { return Injector::inst()->get($this->getTemplateForSelectionOfProducts()); } public function getProductsPerPage(?int $default = 10): int { return (int) $this->recursiveValue('NumberOfProductsPerPage', $default); } /** * work out the recursive value in the Database for SORT / FILTER / DISPLAY. * * @param string $type SORT|FILTER|DISPLAY */ public function getListConfigCalculated(string $type): string { $field = $this->getSortFilterDisplayValues($type, 'dbFieldName'); if ($field) { return $this->recursiveValue($field, BaseApplyer::DEFAULT_NAME); } return BaseApplyer::DEFAULT_NAME; } /** * Returns the number of product groups (children) to show in the current * product list based on the user setting for this page. */ public function getMyLevelOfProductsToShow(?int $defauult = 99): int { $value = $this->recursiveValue('LevelOfProductsToShow', 99); return (int) $value; } /** * Link to the search results. */ public function SearchResultLink(?string $hash = ''): string { if ($hash) { $hash .= '/'; } return $this->Link('searchresults/' . $hash); } /** * This can be set from the controller * to create a filtered baselist. */ public static function set_search_string_for_base_list(int $id, string $string) { self::$searchStringCache[$id] = $string; } /** * Retrieve the base list of products for this group. * * @EcommerceCache candidate? * * @return BaseProductList */ public function getBaseProductList() { if (! $this->baseProductList) { $className = $this->getProductGroupSchema()->getBaseProductListClassName(); $this->baseProductList = $className::inst( $this, $this->getBuyableClassName(), $this->getMyLevelOfProductsToShow(), self::$searchStringCache[$this->ID] ?? '' ); } return $this->baseProductList; } /** * @EcommerceCache candidate? * * @return DataList */ public function getProducts() { return $this->getBaseProductList()->getProducts(); } public function hasProducts(): bool { return $this->getProducts()->exists(); } /** * returns the parent Product Group that is the same type. * So that filters can be set as parent groups. * * @return ProductGroup */ public function MyFilterParent() { if (empty(self::$parentGroupCache[$this->ID])) { self::$parentGroupCache[$this->ID] = $this; while (self::$parentGroupCache[$this->ID]) { $obj = self::$parentGroupCache[$this->ID]; $nextParent = $obj->ParentGroup(); if ($nextParent && $nextParent->ClassName === $this->ClassName) { self::$parentGroupCache[$this->ID] = $nextParent; } else { //important to return here... return self::$parentGroupCache[$this->ID]; } } } return self::$parentGroupCache[$this->ID]; } /** * If products are shown in more than one group then this returns an array * for any products that are linked to this * product group. * * @EcommerceCache candidate? */ public function getProductsToBeIncludedFromOtherGroupsArray(): array { $array = EcommerceCache::inst()->retrieve('AlsoShowProducts_' . $this->ID); if (null === $array) { $array = []; if ($this->getProductsAlsoInOtherGroups() && $this->AlsoShowProducts()->exists()) { $array = $this->AlsoShowProducts()->columnUnique(); } EcommerceCache::inst()->save('AlsoShowProducts_' . $this->ID, $array); } return ArrayMethods::filter_array($array); } public function IDForSearchResults(): int { return $this->ID; } /** * Returns the parent page, but only if it is an instance of Product Group. */ public function MainParentGroup(): ?ProductGroup { return $this->ParentGroup(); } /** * Returns the parent page, but only if it is an instance of Product Group. */ public function ParentGroup(): ?ProductGroup { if (! isset(self::$parentPageCache[$this->ID])) { self::$parentPageCache[$this->ID] = ProductGroup::get_by_id($this->ParentID); } return self::$parentPageCache[$this->ID]; } /** * Returns the parent page, but only if it is an instance of Product Group. */ public function TopParentGroup(): ProductGroup { $parent = $this->ParentGroup(); self::$topParentGroupCache[$this->ID] = $parent && $parent->exists() ? $parent->TopParentGroup() : $this; return self::$topParentGroupCache[$this->ID]; } /** * returns a "BestAvailable" image if the current one is not available * In some cases this is appropriate and in some cases this is not. * For example, consider the following setup * - product A with three variations * - Product A has an image, but the variations have no images * With this scenario, you want to show ONLY the product image * on the product page, but if one of the variations is added to the * cart, then you want to show the product image. * This can be achieved bu using the BestAvailable image. * * @return null|Image */ public function BestAvailableImage() { return $this->recursiveValue('Image'); } /** * tells us if the current page is part of e-commerce. */ public function IsEcommercePage(): bool { return true; } /** * the number of direct descendants. */ public function getNumberOfProducts(): int { if (! isset(self::$getProductCountCache[$this->ID])) { self::$getProductCountCache[$this->ID] = Product::get()->filter(['ParentID' => $this->ID])->count(); } return self::$getProductCountCache[$this->ID]; } /** * Returns the full sortFilterDisplayNames set, a subset, or one value * by either type (e.g. FILTER) or variable (e.g dbFieldName) * or both. * * @param string $typeOrVariable optional SEARCHFILTER|SEARCHFILTER|GROUPFILTER|FILTER|SORT|DISPLAY OR variable * * @return array|string */ public function getSortFilterDisplayValues(?string $typeOrVariable = '', ?string $variable = '') { return $this->getProductGroupSchema()->getSortFilterDisplayValues($typeOrVariable, $variable); } /** * Returns the class we are working with. */ public function getBuyableClassName(): string { return EcommerceConfig::get(ProductGroup::class, 'base_buyable_class'); } /** * Do products occur in more than one group. */ public function getProductsAlsoInOtherGroups(): bool { return EcommerceConfig::inst()->ProductsAlsoInOtherGroups; } /** * Returns children ProductGroup pages of this group. * * @return \SilverStripe\ORM\SS_List (ProductGroups) */ public function ChildCategoriesBasedOnProducts() { return $this->getBaseProductList()->getParentGroupsBasedOnProductsExcludingRootGroup(); } public function ChildCategories(): DataList { return ProductGroup::get() ->filter(['ParentID' => $this->ID, 'ShowInSearch' => true]) ; } public function getShowProductLevelsArray(): array { return $this->getBaseProductList()->getShowProductLevelsArray(); } public function VardumpMe(string $method) { return Vardump::inst()->vardumpMe($this->{$method}(), $method, static::class); } public function onAfterPublish() { parent::onAfterPublish(); if (Config::inst()->get(ProductSearchFilter::class, 'use_product_search_table')) { ProductGroupSearchTable::add_product_group( $this, $this->getProductSearchTableDataValues() ); } } public function onBeforeUnpublish() { ProductGroupSearchTable::remove_product_group($this); } public function onBeforeDelete() { parent::onBeforeDelete(); ProductGroupSearchTable::remove_product_group($this); } protected function getTemplateForSelectionOfProducts(): string { return $this->Config()->get('template_for_selection_of_products'); } protected function addDropDownForListConfig(FieldList $fields, string $type, string $title) { // display style $options = $this->getOptionsForDropdown($type); if (count($options) > 2) { $field = $this->getSortFilterDisplayValues($type, 'dbFieldName'); if ('inherit' === $this->{$field}) { $key = $this->getListConfigCalculated($type); $actualValue = ' (' . ($options[$key] ?? _t('ProductGroup.ERROR', 'ERROR')) . ')'; $options['inherit'] = _t('ProductGroup.INHERIT', 'Inherit') . $actualValue; } $fields->addFieldToTab( 'Root.ProductDisplay', $field = DropdownField::create($field, $title, $options) ); $field->setDescription( _t( 'ProductGroup.INHERIT_RIGHT_TITLE', "Inherit means that the parent page value is used - and if there is no relevant parent page then the site's default value is used." ) ); } } /** * GROUPFILTER: * not available. * * SORT: * returns an array of Key => Title for sort options. * * FILTER: * Returns options for the dropdown of filter options. * * DISPLAY: * Returns the options for product display styles. * In the configuration you can set which ones are available. * If one is available then you must make sure that the corresponding template is available. * For example, if the display style is * MyTemplate => "All Details" * Then you must make sure MyTemplate.ss exists. * * most likely values called: getDefaultFilterOptions,getDefaultSortOrderOptions, getDisplayStyleOptions * * @param string $type - FILTER | SORT | DISPLAY * @param bool $withInherit - optional * * @return array */ protected function getOptionsForDropdown(string $type, ?bool $withInherit = true) { $array = []; if ($withInherit) { $inheritTitle = _t('ProductGroup.INHERIT', 'Inherit'); $array = ['inherit' => $inheritTitle]; } $method = 'get' . ucwords(strtolower($type)) . 'OptionsMap'; $options = $this->getProductGroupSchema()->{$method}(); return array_merge($array, $options); } /** * Used in getCMCFields. * * @return GridField */ protected function getProductGroupsTable() { $field = GridField::create( 'AlsoShowProducts', _t('ProductGroup.OTHER_PRODUCTS_SHOWN_IN_THIS_GROUP', 'Other products shown in this group ...'), $this->AlsoShowProducts(), $config = GridFieldBasicPageRelationConfig::create() ); $ac = $config->getComponentByType(GridFieldAddExistingAutocompleter::class); if ($ac) { $ac->setSearchFields(['Title']); $ac->setResultsFormat('$Breadcrumbs'); $ac->setSearchList(Product::get()->filter(['AllowPurchase' => 1])); } return $field; } /** * get recursive value for Product Group and check EcommerceConfig as last resort. * * @param mixed $default * * @return mixed */ protected function recursiveValue(string $fieldNameOrMethod, $default = null) { $key = $fieldNameOrMethod . '_' . $this->ID; if (! isset(self::$recursiveValuesCache[$key])) { $value = null; $fieldNameOrMethodWithGet = 'get' . $fieldNameOrMethod; $methodWorks = false; foreach ([$fieldNameOrMethod, $fieldNameOrMethodWithGet] as $method) { if ($this->hasMethod($method)) { $methodWorks = true; $outcome = $this->{$method}(); if ($outcome && $outcome instanceof DataObject && $outcome->exists()) { $value = $outcome; } elseif ($outcome) { $value = $outcome; } else { print_r($outcome); user_error($fieldNameOrMethod . ' is empty'); } } } if (false === $methodWorks) { $value = $this->{$fieldNameOrMethod} ?? null; } if (! $value || 'inherit' === $value) { $parent = $this->ParentGroup(); if ($parent && $parent->exists() && $parent->ID !== $this->ID) { $value = $parent->recursiveValue($fieldNameOrMethod, $default); } else { $value = EcommerceConfig::inst()->recursiveValue($fieldNameOrMethod, $default); } } if (! $value) { $value = $default; } self::$recursiveValuesCache[$key] = $value; } return self::$recursiveValuesCache[$key]; } protected function getProductSearchTableDataValues(): array { return [ $this->Title, $this->Content, ]; } } |