diff --git a/src/Plugin.php b/src/Plugin.php index 7925e88b..166e777a 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -7,6 +7,7 @@ use craft\events\RegisterUrlRulesEvent; use craft\feedme\base\PluginTrait; use craft\feedme\models\Settings; +use craft\feedme\services\FeedConfigStorage; use craft\feedme\services\DataTypes; use craft\feedme\services\Elements; use craft\feedme\services\Feeds; @@ -26,6 +27,7 @@ /** * Class Plugin * + * @property-read FeedConfigStorage $config * @property-read DataTypes $data * @property-read Elements $elements * @property-read Feeds $feeds @@ -50,6 +52,7 @@ public static function config(): array { return [ 'components' => [ + 'config' => ['class' => FeedConfigStorage::class], 'data' => ['class' => DataTypes::class], 'elements' => ['class' => Elements::class], 'feeds' => ['class' => Feeds::class], diff --git a/src/base/PluginTrait.php b/src/base/PluginTrait.php index cdd62783..5ac1d3ed 100644 --- a/src/base/PluginTrait.php +++ b/src/base/PluginTrait.php @@ -4,6 +4,7 @@ use Craft; use craft\feedme\Plugin; +use craft\feedme\services\FeedConfigStorage; use craft\feedme\services\DataTypes; use craft\feedme\services\Elements; use craft\feedme\services\Feeds; @@ -78,6 +79,15 @@ public static function debug($message): void // Public Methods // ========================================================================= + /** + * @return FeedConfigStorage + * @throws \yii\base\InvalidConfigException + */ + public function getFeedConfigStorage(): FeedConfigStorage + { + return $this->get('feedConfigStorage'); + } + /** * @return DataTypes * @throws \yii\base\InvalidConfigException diff --git a/src/console/controllers/FeedConfigStorageController.php b/src/console/controllers/FeedConfigStorageController.php new file mode 100644 index 00000000..317e89b4 --- /dev/null +++ b/src/console/controllers/FeedConfigStorageController.php @@ -0,0 +1,66 @@ +config->write() ? ExitCode::CANTCREAT : ExitCode::OK; + } + + /** + * Reads feeds records from file + * + * @return int + */ + public function actionRead(): int + { + $result = Plugin::$plugin->config->read(); + + if($result->success) { + return ExitCode::OK; + } + + $this->stderr("FAILED FEEDS" . PHP_EOL, Console::FG_RED); + foreach($result->failed_feeds as $feed) { + $this->stderr("({$feed->id}) {$feed->name}".PHP_EOL, Console::FG_RED); + } + + if(count($result->success_feeds) > 0) { + $this->stdout(PHP_EOL . "SUCCESSFUL FEEDS" . PHP_EOL, Console::FG_GREEN); + foreach ($result->success_feeds as $feed) { + $this->stdout("({$feed->id}) {$feed->name} " . PHP_EOL, Console::FG_GREEN); + } + } + + return ExitCode::UNSPECIFIED_ERROR; + } +} diff --git a/src/console/controllers/FeedsController.php b/src/console/controllers/FeedsController.php index be29ded4..280de6ec 100644 --- a/src/console/controllers/FeedsController.php +++ b/src/console/controllers/FeedsController.php @@ -9,6 +9,8 @@ use yii\console\ExitCode; /** + * Queues up feed imports + * * @property Plugin $module */ class FeedsController extends Controller diff --git a/src/elements/Entry.php b/src/elements/Entry.php index c03b2992..f1990c0c 100644 --- a/src/elements/Entry.php +++ b/src/elements/Entry.php @@ -102,10 +102,15 @@ public function getQuery($settings, array $params = []): mixed $section = $this->element->getSection(); } + list($sectionId, $entryTypeId) = $this->getSectionAndElementGroupIdsFromUids( + $settings['elementGroup'][EntryElement::class]['section'], + $settings['elementGroup'][EntryElement::class]['entryType'] + ); + $query = EntryElement::find() ->status(null) - ->sectionId($settings['elementGroup'][EntryElement::class]['section']) - ->typeId($settings['elementGroup'][EntryElement::class]['entryType']); + ->sectionId($sectionId) + ->typeId($entryTypeId); if (isset($section) && $section->propagationMethod === Section::PROPAGATION_METHOD_CUSTOM) { $query->site('*') @@ -119,16 +124,37 @@ public function getQuery($settings, array $params = []): mixed return $query; } + + public function getSectionAndElementGroupIdsFromUids(string $sectionUid, string $elementGroupUid): array + { + $section = Craft::$app->sections->getSectionByUid($sectionUid); + $entryTypes = $section->getEntryTypes(); + $entryTypeId = null; + foreach ($entryTypes as $entryType) { + if ($entryType->uid === $elementGroupUid) { + $entryTypeId = $entryType->id; + break; + } + } + return [$section->id, $entryTypeId]; + } + /** * @inheritDoc */ public function setModel($settings): ElementInterface { $this->element = new EntryElement(); - $this->element->sectionId = $settings['elementGroup'][EntryElement::class]['section']; - $this->element->typeId = $settings['elementGroup'][EntryElement::class]['entryType']; - $section = Craft::$app->getSections()->getSectionById($this->element->sectionId); + list($sectionId, $entryTypeId) = $this->getSectionAndElementGroupIdsFromUids( + $settings['elementGroup'][EntryElement::class]['section'], + $settings['elementGroup'][EntryElement::class]['entryType'] + ); + + $this->element->sectionId = $sectionId; + $this->element->typeId = $entryTypeId; + + $section = Craft::$app->getSections()->getSectionByUid($this->element->section->uid); $siteId = Hash::get($settings, 'siteId'); if ($siteId) { diff --git a/src/fields/Entries.php b/src/fields/Entries.php index 6cf59dbc..e781d54a 100644 --- a/src/fields/Entries.php +++ b/src/fields/Entries.php @@ -224,7 +224,7 @@ private function _createElement($dataValue): ?int $element->typeId = $typeId; $siteId = Hash::get($this->feed, 'siteId'); - $section = Craft::$app->getSections()->getSectionById($element->sectionId); + $section = Craft::$app->getSections()->getSectionByUid($element->sectionUid); if ($siteId) { $element->siteId = $siteId; diff --git a/src/models/FeedModel.php b/src/models/FeedModel.php index 831dfd24..7c12afd2 100644 --- a/src/models/FeedModel.php +++ b/src/models/FeedModel.php @@ -10,6 +10,8 @@ use craft\feedme\base\ElementInterface; use craft\feedme\helpers\DuplicateHelper; use craft\feedme\Plugin; +use craft\helpers\Db; +use craft\helpers\Json; use DateTime; /** @@ -254,4 +256,51 @@ public function rules(): array [['backup', 'setEmptyValues'], 'boolean'], ]; } + + /** + * Retrieves the attributes in a format for a record + * + * @return array An array containing the record attributes + */ + public function getRecordAttributes(): array + { + $attributes = [ + "name" => $this->name, + "feedUrl" => $this->feedUrl, + "feedType" => $this->feedType, + "primaryElement" => $this->primaryElement, + "elementType" => $this->elementType, + "siteId" => $this->siteId, + "singleton" => $this->singleton, + "duplicateHandle" => $this->duplicateHandle, + "updateSearchIndexes" => $this->updateSearchIndexes, + "paginationNode" => $this->paginationNode, + "passkey" => $this->passkey, + "backup" => $this->backup, + "setEmptyValues" => $this->setEmptyValues, + + // These might be automatically updated, but in the case of writing + // the feed record to a file, we want these values. + "dateCreated" => Db::prepareDateForDb($this->dateCreated), + "dateUpdated" => Db::prepareDateForDb($this->dateUpdated) + ]; + + if ($this->elementGroup) { + $attributes["elementGroup"] = Json::encode($this->elementGroup); + } + + if ($this->duplicateHandle) { + $attributes["duplicateHandle"] = Json::encode($this->duplicateHandle); + } + + if ($this->fieldMapping) { + $attributes["fieldMapping"] = Json::encode($this->fieldMapping); + } + + if ($this->fieldUnique) { + $attributes["fieldUnique"] = Json::encode($this->fieldUnique); + } + + return $attributes; + } } diff --git a/src/services/DataTypes.php b/src/services/DataTypes.php index 0ec71b8f..79d8bbdf 100644 --- a/src/services/DataTypes.php +++ b/src/services/DataTypes.php @@ -234,10 +234,15 @@ public function getRawData($url, $feedId = null): array */ public function getFeedData($feedModel, bool $usePrimaryElement = true): mixed { - $feedDataResponse = $feedModel->getDataType()->getFeed($feedModel->feedUrl, $feedModel, $usePrimaryElement); + // Dynamism in the feedUrl -- use twig to inject dates, for example + $twig = \Craft::$app->getView()->getTwig(); + $template = twig_template_from_string($twig, $feedModel->feedUrl); + $feedUrl = $template->render([]); + + $feedDataResponse = $feedModel->getDataType()->getFeed($feedUrl, $feedModel, $usePrimaryElement); $event = new FeedDataEvent([ - 'url' => $feedModel->feedUrl, + 'url' => $feedUrl, 'response' => $feedDataResponse, 'feedId' => $feedModel->id, ]); diff --git a/src/services/FeedConfigStorage.php b/src/services/FeedConfigStorage.php new file mode 100644 index 00000000..c99f5aa9 --- /dev/null +++ b/src/services/FeedConfigStorage.php @@ -0,0 +1,78 @@ + $feed->id] + $feed->getRecordAttributes(); }, + Plugin::getInstance()->getFeeds()->getFeeds() + ); + + $fileName = $this->defaultFileName; + $fileContents = Yaml::dump($feeds, JSON_PRETTY_PRINT); + + if(!file_exists($this->defaultFileName)) mkdir(dirname($this->defaultFileName), 0777, true); + + return file_put_contents($fileName, $fileContents) !== false; + } + + /** + * Reads feed data from a YAML file and imports each feed. + * + * @return \stdClass The result object containing success flag, successful feeds, and failed feeds. + * @throws \yii\base\InvalidConfigException If the configuration is invalid. + */ + public function read(): \stdClass { + $result = (object)["success" => true, "success_feeds" => [], "failed_feeds" => []]; + + // Delete all feeds + foreach(Plugin::getInstance()->getFeeds()->getFeeds() as $feed) { + Plugin::getInstance()->getFeeds()->deleteFeedById($feed['id']); + } + + $fileName = $this->defaultFileName; + $feeds = Yaml::parse(file_get_contents($fileName)); + foreach ($feeds as $feed) { + $record = new FeedRecord(); + $record->setAttributes($feed, false); + $record->save(false); + $success = (bool) $record->id; + if ($success) { + $result->success_feeds[] = $record; + } else { + $result->failed_feeds[] = $record; + } + } + + if(count($result->failed_feeds) > 0) { + $result->success = false; + } + + return $result; + } +} diff --git a/src/services/Feeds.php b/src/services/Feeds.php index f0defbf3..17670c90 100644 --- a/src/services/Feeds.php +++ b/src/services/Feeds.php @@ -119,30 +119,8 @@ public function saveFeed(FeedModel $model, bool $runValidation = true): bool } } - $record->name = $model->name; - $record->feedUrl = $model->feedUrl; - $record->feedType = $model->feedType; - $record->primaryElement = $model->primaryElement; - $record->elementType = $model->elementType; - $record->siteId = $model->siteId; - $record->singleton = $model->singleton; - $record->duplicateHandle = $model->duplicateHandle; - $record->updateSearchIndexes = $model->updateSearchIndexes; - $record->paginationNode = $model->paginationNode; - $record->passkey = $model->passkey; - $record->backup = $model->backup; - $record->setEmptyValues = $model->setEmptyValues; - - if ($model->elementGroup) { - $record->setAttribute('elementGroup', Json::encode($model->elementGroup)); - } - - if ($model->fieldMapping) { - $record->setAttribute('fieldMapping', Json::encode($model->fieldMapping)); - } - - if ($model->fieldUnique) { - $record->setAttribute('fieldUnique', Json::encode($model->fieldUnique)); + foreach($model->getRecordAttributes() as $name => $value) { + $record->setAttribute($name, $value); } if ($isNewModel) { diff --git a/src/templates/_includes/elements/entries/column.html b/src/templates/_includes/elements/entries/column.html index b3306e9c..67668511 100644 --- a/src/templates/_includes/elements/entries/column.html +++ b/src/templates/_includes/elements/entries/column.html @@ -1,9 +1,9 @@ -{% set sectionId = feed.elementGroup[elementType].section %} -{% set entryTypeId = feed.elementGroup[elementType].entryType %} +{% set sectionUid = feed.elementGroup[elementType].section %} +{% set entryTypeUid = feed.elementGroup[elementType].entryType %} -{% if sectionId and entryTypeId %} - {% set section = craft.app.sections.getSectionById(sectionId) %} - {% set entryType = craft.app.sections.getEntryTypeById(entryTypeId) %} +{% if sectionUid and entryTypeUid %} + {% set section = craft.app.sections.getSectionByUid(sectionUid) %} + {% set entryType = craft.app.sections.getEntryTypeByUid(entryTypeUid) %} {% if section and entryType %} {{ section.name }} diff --git a/src/templates/_includes/elements/entries/groups.html b/src/templates/_includes/elements/entries/groups.html index ae20ad41..3d05e6b8 100644 --- a/src/templates/_includes/elements/entries/groups.html +++ b/src/templates/_includes/elements/entries/groups.html @@ -7,24 +7,22 @@ {# Create a section-indexed array of element types #} {% set entryTypes = [] %} {% for section in sections %} - {% set options = craft.feedme.getSelectOptions(section.model.getEntryTypes()) %} - + {% set options = craft.feedme.getSelectOptions(section.model.getEntryTypes(), 'name', 'uid') %} {# We have to prefix the index, otherwise Twig doesn't maintain numbered index correctly #} - {% set entryTypes = entryTypes|merge({ ('item_' ~ section.id): options }) %} + {% set entryTypes = entryTypes|merge({ ('item_' ~ section.model.uid): options }) %} {% endfor %} -{% set sectionId = null %} -{% set entryTypeId = null %} +{% set sectionUid = null %} +{% set entryTypeUid = null %} {# Load saved values for feed #} {% if feed.elementGroup[elementType] is defined %} - {% set sectionId = feed.elementGroup[elementType].section ?? null %} - {% set entryTypeId = feed.elementGroup[elementType].entryType ?? null %} + {% set sectionUid = feed.elementGroup[elementType].section ?? null %} + {% set entryTypeUid = feed.elementGroup[elementType].entryType ?? null %} {% endif %} -{% if sectionId %} - {% set section = craft.app.sections.getSectionById(sectionId) %} - +{% if sectionUid %} + {% set section = craft.app.sections.getSectionByUid(sectionUid) %} {% if section %} {% set sectionEntryTypes = section.getEntryTypes() %} {% endif %} @@ -39,8 +37,8 @@ class: 'element-parent-group', id: 'elementGroup-' ~ elementType ~ '-section', name: 'elementGroup[' ~ elementType ~ '][section]', - options: craft.feedme.getSelectOptions(sections|map(s => s.model)), - value: sectionId ?? '', + options: craft.feedme.getSelectOptions(sections|map(s => s.model), 'name', 'uid'), + value: sectionUid ?? '', errors: feed.getErrors('elementGroup'), required: true, }) }} @@ -51,8 +49,8 @@ class: 'element-child-group', id: 'elementGroup-' ~ elementType ~ '-entryType', name: 'elementGroup[' ~ elementType ~ '][entryType]', - options: craft.feedme.getSelectOptions(sectionEntryTypes), - value: entryTypeId ?? '', + options: craft.feedme.getSelectOptions(sectionEntryTypes, 'name', 'uid'), + value: entryTypeUid ?? '', errors: feed.getErrors('elementGroup'), required: true, }) }} diff --git a/src/templates/_includes/elements/entries/map.html b/src/templates/_includes/elements/entries/map.html index cb7cd06a..7d157b29 100644 --- a/src/templates/_includes/elements/entries/map.html +++ b/src/templates/_includes/elements/entries/map.html @@ -2,11 +2,11 @@ {% import 'feed-me/_macros' as feedMeMacro %} {% if feed.elementGroup %} - {% set sectionId = feed.elementGroup[feed.elementType].section %} - {% set entryTypeId = feed.elementGroup[feed.elementType].entryType %} + {% set sectionUid = feed.elementGroup[feed.elementType].section %} + {% set entryTypeUid = feed.elementGroup[feed.elementType].entryType %} - {% set section = craft.app.sections.getSectionById(sectionId) %} - {% set entryType = craft.app.sections.getEntryTypeById(entryTypeId) %} + {% set section = craft.app.sections.getSectionByUid(sectionUid) %} + {% set entryType = craft.app.sections.getEntryTypeByUid(entryTypeUid) %} {% endif %}