From 95e63d52a476987254a5a30dca2a821673da5d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20M=C3=BCller?= <2566282+brotkrueml@users.noreply.github.com> Date: Sat, 21 Mar 2020 14:16:59 +0100 Subject: [PATCH] [TASK] Possibility to register additional schema types Related: #38 --- .../Model/WebPageElementTypeInterface.php | 18 + Classes/Model/Type/SiteNavigationElement.php | 3 +- Classes/Model/Type/Table.php | 3 +- Classes/Model/Type/WPAdBlock.php | 3 +- Classes/Model/Type/WPFooter.php | 3 +- Classes/Model/Type/WPHeader.php | 3 +- Classes/Model/Type/WPSideBar.php | 3 +- Classes/Model/Type/WebPageElement.php | 3 +- Classes/Provider/TypesProvider.php | 756 +++--------------- Classes/Provider/WebPageTypeProvider.php | 24 +- Configuration/TxSchema/TypeModels.php | 604 ++++++++++++++ .../Classes/Configuration/Configuration.php | 2 +- Generator/Classes/Generator.php | 17 +- Generator/Templates/Type.php.twig | 6 + Generator/Templates/TypeModels.php.twig | 6 + Generator/Templates/TypesProvider.php.twig | 79 -- Generator/generate.php | 2 +- .../Configuration/TxSchema/TypeModels.php | 9 + Tests/Fixtures/Model/Type/BreadcrumbList.php | 10 + Tests/Fixtures/Model/Type/ItemPage.php | 3 +- Tests/Fixtures/Model/Type/Table.php | 11 + Tests/Fixtures/Model/Type/VideoGallery.php | 3 +- Tests/Fixtures/Model/Type/WebPage.php | 3 +- Tests/Fixtures/Model/Type/WebSite.php | 10 + Tests/Unit/Provider/TypesProviderTest.php | 260 ++++++ .../Unit/Provider/WebPageTypeProviderTest.php | 64 +- ext_localconf.php | 13 +- 27 files changed, 1139 insertions(+), 782 deletions(-) create mode 100644 Classes/Core/Model/WebPageElementTypeInterface.php create mode 100644 Configuration/TxSchema/TypeModels.php create mode 100644 Generator/Templates/TypeModels.php.twig delete mode 100644 Generator/Templates/TypesProvider.php.twig create mode 100644 Tests/Fixtures/Configuration/TxSchema/TypeModels.php create mode 100644 Tests/Fixtures/Model/Type/BreadcrumbList.php create mode 100644 Tests/Fixtures/Model/Type/Table.php create mode 100644 Tests/Fixtures/Model/Type/WebSite.php create mode 100644 Tests/Unit/Provider/TypesProviderTest.php diff --git a/Classes/Core/Model/WebPageElementTypeInterface.php b/Classes/Core/Model/WebPageElementTypeInterface.php new file mode 100644 index 00000000..01bf5c45 --- /dev/null +++ b/Classes/Core/Model/WebPageElementTypeInterface.php @@ -0,0 +1,18 @@ + null, diff --git a/Classes/Model/Type/Table.php b/Classes/Model/Type/Table.php index 27e15101..7c8c486b 100644 --- a/Classes/Model/Type/Table.php +++ b/Classes/Model/Type/Table.php @@ -11,11 +11,12 @@ */ use Brotkrueml\Schema\Core\Model\AbstractType; +use Brotkrueml\Schema\Core\Model\WebPageElementTypeInterface; /** * A table on a Web page. */ -final class Table extends AbstractType +final class Table extends AbstractType implements WebPageElementTypeInterface { protected $properties = [ 'about' => null, diff --git a/Classes/Model/Type/WPAdBlock.php b/Classes/Model/Type/WPAdBlock.php index c5b376ea..56f9ad79 100644 --- a/Classes/Model/Type/WPAdBlock.php +++ b/Classes/Model/Type/WPAdBlock.php @@ -11,11 +11,12 @@ */ use Brotkrueml\Schema\Core\Model\AbstractType; +use Brotkrueml\Schema\Core\Model\WebPageElementTypeInterface; /** * An advertising section of the page. */ -final class WPAdBlock extends AbstractType +final class WPAdBlock extends AbstractType implements WebPageElementTypeInterface { protected $properties = [ 'about' => null, diff --git a/Classes/Model/Type/WPFooter.php b/Classes/Model/Type/WPFooter.php index aeb7874f..dab7cfb1 100644 --- a/Classes/Model/Type/WPFooter.php +++ b/Classes/Model/Type/WPFooter.php @@ -11,11 +11,12 @@ */ use Brotkrueml\Schema\Core\Model\AbstractType; +use Brotkrueml\Schema\Core\Model\WebPageElementTypeInterface; /** * The footer section of the page. */ -final class WPFooter extends AbstractType +final class WPFooter extends AbstractType implements WebPageElementTypeInterface { protected $properties = [ 'about' => null, diff --git a/Classes/Model/Type/WPHeader.php b/Classes/Model/Type/WPHeader.php index 75e9c5bd..7a829f0f 100644 --- a/Classes/Model/Type/WPHeader.php +++ b/Classes/Model/Type/WPHeader.php @@ -11,11 +11,12 @@ */ use Brotkrueml\Schema\Core\Model\AbstractType; +use Brotkrueml\Schema\Core\Model\WebPageElementTypeInterface; /** * The header section of the page. */ -final class WPHeader extends AbstractType +final class WPHeader extends AbstractType implements WebPageElementTypeInterface { protected $properties = [ 'about' => null, diff --git a/Classes/Model/Type/WPSideBar.php b/Classes/Model/Type/WPSideBar.php index d90668dd..928c58d4 100644 --- a/Classes/Model/Type/WPSideBar.php +++ b/Classes/Model/Type/WPSideBar.php @@ -11,11 +11,12 @@ */ use Brotkrueml\Schema\Core\Model\AbstractType; +use Brotkrueml\Schema\Core\Model\WebPageElementTypeInterface; /** * A sidebar section of the page. */ -final class WPSideBar extends AbstractType +final class WPSideBar extends AbstractType implements WebPageElementTypeInterface { protected $properties = [ 'about' => null, diff --git a/Classes/Model/Type/WebPageElement.php b/Classes/Model/Type/WebPageElement.php index 4a3e0daf..f8baba58 100644 --- a/Classes/Model/Type/WebPageElement.php +++ b/Classes/Model/Type/WebPageElement.php @@ -11,11 +11,12 @@ */ use Brotkrueml\Schema\Core\Model\AbstractType; +use Brotkrueml\Schema\Core\Model\WebPageElementTypeInterface; /** * A web page element, like a table or an image. */ -final class WebPageElement extends AbstractType +final class WebPageElement extends AbstractType implements WebPageElementTypeInterface { protected $properties = [ 'about' => null, diff --git a/Classes/Provider/TypesProvider.php b/Classes/Provider/TypesProvider.php index 81246789..ceddc1d1 100644 --- a/Classes/Provider/TypesProvider.php +++ b/Classes/Provider/TypesProvider.php @@ -10,624 +10,102 @@ * LICENSE.txt file that was distributed with this source code. */ +use Brotkrueml\Schema\Core\Model\WebPageElementTypeInterface; +use Brotkrueml\Schema\Core\Model\WebPageTypeInterface; +use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; +use TYPO3\CMS\Core\Package\PackageManager; +use TYPO3\CMS\Core\Utility\GeneralUtility; + /** - * Provide names of all types or a subset of them + * Provide names of all available types or a subset of them * - * The lists of types are generated out of the official schema definition + * The lists of types are generated from the official schema definition + * or added in extensions via Configuration/TxSchema/TypeModels.php * * @api */ final class TypesProvider { + private const CACHE_IDENTIFIER = 'tx_schema_core'; + private const CACHE_ENTRY_IDENTIFIER_TYPES = 'types'; + private const CACHE_ENTRY_IDENTIFIER_WEBPAGE_TYPES = 'webpage_types'; + private const CACHE_ENTRY_IDENTIFIER_WEBPAGEELEMENT_TYPES = 'webpageelement_types'; + + private static $types = []; + private static $webPageTypes = []; + private static $webPageElementTypes = []; + + /** @var FrontendInterface */ + private $cache; + + /** @var PackageManager */ + private $packageManager; + + /** + * @param CacheManager|null $cacheManager For test purposes + * @param PackageManager|null $packageManager For test purposes + * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + */ + public function __construct(CacheManager $cacheManager = null, PackageManager $packageManager = null) + { + $cacheManager = $cacheManager ?? GeneralUtility::makeInstance(CacheManager::class); + + $this->cache = $cacheManager->getCache(static::CACHE_IDENTIFIER); + $this->packageManager = $packageManager ?? GeneralUtility::makeInstance(PackageManager::class); + } + /** * Get all available types - * @see https://schema.org/docs/full.html */ public function getTypes(): array { - return [ - 'AMRadioChannel', - 'APIReference', - 'AboutPage', - 'AcceptAction', - 'Accommodation', - 'AccountingService', - 'AchieveAction', - 'Action', - 'ActionAccessSpecification', - 'ActionStatusType', - 'ActivateAction', - 'AddAction', - 'AdministrativeArea', - 'AdultEntertainment', - 'AggregateOffer', - 'AggregateRating', - 'AgreeAction', - 'Airline', - 'Airport', - 'AlignmentObject', - 'AllocateAction', - 'AmusementPark', - 'AnimalShelter', - 'Answer', - 'Apartment', - 'ApartmentComplex', - 'AppendAction', - 'ApplyAction', - 'Aquarium', - 'ArriveAction', - 'ArtGallery', - 'Article', - 'AskAction', - 'AssessAction', - 'AssignAction', - 'Attorney', - 'Audience', - 'AudioObject', - 'AuthorizeAction', - 'AutoBodyShop', - 'AutoDealer', - 'AutoPartsStore', - 'AutoRental', - 'AutoRepair', - 'AutoWash', - 'AutomatedTeller', - 'AutomotiveBusiness', - 'Bakery', - 'BankAccount', - 'BankOrCreditUnion', - 'BarOrPub', - 'Barcode', - 'Beach', - 'BeautySalon', - 'BedAndBreakfast', - 'BedDetails', - 'BedType', - 'BefriendAction', - 'BikeStore', - 'Blog', - 'BlogPosting', - 'BoardingPolicyType', - 'BodyOfWater', - 'Book', - 'BookFormatType', - 'BookSeries', - 'BookStore', - 'BookmarkAction', - 'BorrowAction', - 'BowlingAlley', - 'Brand', - 'BreadcrumbList', - 'Brewery', - 'Bridge', - 'BroadcastChannel', - 'BroadcastEvent', - 'BroadcastFrequencySpecification', - 'BroadcastService', - 'BuddhistTemple', - 'BusReservation', - 'BusStation', - 'BusStop', - 'BusTrip', - 'BusinessAudience', - 'BusinessEntityType', - 'BusinessEvent', - 'BusinessFunction', - 'BuyAction', - 'CableOrSatelliteService', - 'CafeOrCoffeeShop', - 'Campground', - 'CampingPitch', - 'Canal', - 'CancelAction', - 'Car', - 'Casino', - 'CatholicChurch', - 'Cemetery', - 'CheckAction', - 'CheckInAction', - 'CheckOutAction', - 'CheckoutPage', - 'ChildCare', - 'ChildrensEvent', - 'ChooseAction', - 'Church', - 'City', - 'CityHall', - 'CivicStructure', - 'ClaimReview', - 'Clip', - 'ClothingStore', - 'CollectionPage', - 'CollegeOrUniversity', - 'ComedyClub', - 'ComedyEvent', - 'Comment', - 'CommentAction', - 'CommunicateAction', - 'CompoundPriceSpecification', - 'ComputerLanguage', - 'ComputerStore', - 'ConfirmAction', - 'ConsumeAction', - 'ContactPage', - 'ContactPoint', - 'ContactPointOption', - 'Continent', - 'ControlAction', - 'ConvenienceStore', - 'Conversation', - 'CookAction', - 'Corporation', - 'Country', - 'Course', - 'CourseInstance', - 'Courthouse', - 'CreateAction', - 'CreativeWork', - 'CreativeWorkSeason', - 'CreativeWorkSeries', - 'CreditCard', - 'Crematorium', - 'CurrencyConversionService', - 'DanceEvent', - 'DanceGroup', - 'DataCatalog', - 'DataDownload', - 'DataFeed', - 'DataFeedItem', - 'Dataset', - 'DayOfWeek', - 'DaySpa', - 'DeactivateAction', - 'DefenceEstablishment', - 'DeleteAction', - 'DeliveryChargeSpecification', - 'DeliveryEvent', - 'DeliveryMethod', - 'Demand', - 'Dentist', - 'DepartAction', - 'DepartmentStore', - 'DepositAccount', - 'DigitalDocument', - 'DigitalDocumentPermission', - 'DigitalDocumentPermissionType', - 'DisagreeAction', - 'DiscoverAction', - 'DiscussionForumPosting', - 'DislikeAction', - 'Distance', - 'Distillery', - 'DonateAction', - 'DownloadAction', - 'DrawAction', - 'DrinkAction', - 'DriveWheelConfigurationValue', - 'DryCleaningOrLaundry', - 'Duration', - 'EatAction', - 'EducationEvent', - 'EducationalAudience', - 'EducationalOrganization', - 'Electrician', - 'ElectronicsStore', - 'ElementarySchool', - 'EmailMessage', - 'Embassy', - 'EmergencyService', - 'EmployeeRole', - 'EmployerAggregateRating', - 'EmploymentAgency', - 'EndorseAction', - 'EndorsementRating', - 'Energy', - 'EngineSpecification', - 'EntertainmentBusiness', - 'EntryPoint', - 'Enumeration', - 'Episode', - 'Event', - 'EventReservation', - 'EventStatusType', - 'EventVenue', - 'ExerciseAction', - 'ExerciseGym', - 'ExhibitionEvent', - 'FAQPage', - 'FMRadioChannel', - 'FastFoodRestaurant', - 'Festival', - 'FilmAction', - 'FinancialProduct', - 'FinancialService', - 'FindAction', - 'FireStation', - 'Flight', - 'FlightReservation', - 'Florist', - 'FollowAction', - 'FoodEstablishment', - 'FoodEstablishmentReservation', - 'FoodEvent', - 'FoodService', - 'FurnitureStore', - 'Game', - 'GamePlayMode', - 'GameServer', - 'GameServerStatus', - 'GardenStore', - 'GasStation', - 'GatedResidenceCommunity', - 'GenderType', - 'GeneralContractor', - 'GeoCircle', - 'GeoCoordinates', - 'GeoShape', - 'GiveAction', - 'GolfCourse', - 'GovernmentBuilding', - 'GovernmentOffice', - 'GovernmentOrganization', - 'GovernmentPermit', - 'GovernmentService', - 'GroceryStore', - 'HVACBusiness', - 'HairSalon', - 'HardwareStore', - 'HealthAndBeautyBusiness', - 'HealthClub', - 'HighSchool', - 'HinduTemple', - 'HobbyShop', - 'HomeAndConstructionBusiness', - 'HomeGoodsStore', - 'Hospital', - 'Hostel', - 'Hotel', - 'HotelRoom', - 'House', - 'HousePainter', - 'HowTo', - 'HowToDirection', - 'HowToItem', - 'HowToSection', - 'HowToStep', - 'HowToSupply', - 'HowToTip', - 'HowToTool', - 'IceCreamShop', - 'IgnoreAction', - 'ImageGallery', - 'ImageObject', - 'IndividualProduct', - 'InformAction', - 'InsertAction', - 'InstallAction', - 'InsuranceAgency', - 'Intangible', - 'InteractAction', - 'InteractionCounter', - 'InternetCafe', - 'InvestmentOrDeposit', - 'InviteAction', - 'Invoice', - 'ItemAvailability', - 'ItemList', - 'ItemListOrderType', - 'ItemPage', - 'JewelryStore', - 'JobPosting', - 'JoinAction', - 'LakeBodyOfWater', - 'Landform', - 'LandmarksOrHistoricalBuildings', - 'Language', - 'LeaveAction', - 'LegalService', - 'LegislativeBuilding', - 'LendAction', - 'Library', - 'LikeAction', - 'LiquorStore', - 'ListItem', - 'ListenAction', - 'LiteraryEvent', - 'LiveBlogPosting', - 'LoanOrCredit', - 'LocalBusiness', - 'LocationFeatureSpecification', - 'LockerDelivery', - 'Locksmith', - 'LodgingBusiness', - 'LodgingReservation', - 'LoseAction', - 'Map', - 'MapCategoryType', - 'MarryAction', - 'Mass', - 'MediaGallery', - 'MediaObject', - 'MediaSubscription', - 'MedicalOrganization', - 'MeetingRoom', - 'MensClothingStore', - 'Menu', - 'MenuItem', - 'MenuSection', - 'Message', - 'MiddleSchool', - 'MobileApplication', - 'MobilePhoneStore', - 'MonetaryAmount', - 'MonetaryAmountDistribution', - 'Mosque', - 'Motel', - 'MotorcycleDealer', - 'MotorcycleRepair', - 'Mountain', - 'MoveAction', - 'Movie', - 'MovieClip', - 'MovieRentalStore', - 'MovieSeries', - 'MovieTheater', - 'MovingCompany', - 'Museum', - 'MusicAlbum', - 'MusicAlbumProductionType', - 'MusicAlbumReleaseType', - 'MusicComposition', - 'MusicEvent', - 'MusicGroup', - 'MusicPlaylist', - 'MusicRecording', - 'MusicRelease', - 'MusicReleaseFormatType', - 'MusicStore', - 'MusicVenue', - 'MusicVideoObject', - 'NGO', - 'NailSalon', - 'NewsArticle', - 'NightClub', - 'Notary', - 'NoteDigitalDocument', - 'NutritionInformation', - 'Occupation', - 'OceanBodyOfWater', - 'Offer', - 'OfferCatalog', - 'OfferItemCondition', - 'OfficeEquipmentStore', - 'OnDemandEvent', - 'OpeningHoursSpecification', - 'Order', - 'OrderAction', - 'OrderItem', - 'OrderStatus', - 'Organization', - 'OrganizationRole', - 'OrganizeAction', - 'OutletStore', - 'OwnershipInfo', - 'PaintAction', - 'Painting', - 'ParcelDelivery', - 'ParcelService', - 'ParentAudience', - 'Park', - 'ParkingFacility', - 'PawnShop', - 'PayAction', - 'PaymentCard', - 'PaymentChargeSpecification', - 'PaymentMethod', - 'PaymentService', - 'PaymentStatusType', - 'PeopleAudience', - 'PerformAction', - 'PerformanceRole', - 'PerformingArtsTheater', - 'PerformingGroup', - 'Periodical', - 'Permit', - 'Person', - 'PetStore', - 'Pharmacy', - 'Photograph', - 'PhotographAction', - 'Physician', - 'Place', - 'PlaceOfWorship', - 'PlanAction', - 'PlayAction', - 'Playground', - 'Plumber', - 'PoliceStation', - 'Pond', - 'PostOffice', - 'PostalAddress', - 'PreOrderAction', - 'PrependAction', - 'Preschool', - 'PresentationDigitalDocument', - 'PriceSpecification', - 'Product', - 'ProductModel', - 'ProfessionalService', - 'ProfilePage', - 'ProgramMembership', - 'PropertyValue', - 'PropertyValueSpecification', - 'PublicSwimmingPool', - 'PublicationEvent', - 'PublicationIssue', - 'PublicationVolume', - 'QAPage', - 'QualitativeValue', - 'QuantitativeValue', - 'QuantitativeValueDistribution', - 'Quantity', - 'Question', - 'QuoteAction', - 'RVPark', - 'RadioChannel', - 'RadioClip', - 'RadioEpisode', - 'RadioSeason', - 'RadioSeries', - 'RadioStation', - 'Rating', - 'ReactAction', - 'ReadAction', - 'RealEstateAgent', - 'ReceiveAction', - 'Recipe', - 'RecyclingCenter', - 'RegisterAction', - 'RejectAction', - 'RentAction', - 'RentalCarReservation', - 'ReplaceAction', - 'ReplyAction', - 'Report', - 'Reservation', - 'ReservationPackage', - 'ReservationStatusType', - 'ReserveAction', - 'Reservoir', - 'Residence', - 'Resort', - 'Restaurant', - 'RestrictedDiet', - 'ResumeAction', - 'ReturnAction', - 'Review', - 'ReviewAction', - 'RiverBodyOfWater', - 'Role', - 'RoofingContractor', - 'Room', - 'RsvpAction', - 'RsvpResponseType', - 'SaleEvent', - 'ScheduleAction', - 'ScholarlyArticle', - 'School', - 'ScreeningEvent', - 'Sculpture', - 'SeaBodyOfWater', - 'SearchAction', - 'SearchResultsPage', - 'Seat', - 'SelfStorage', - 'SellAction', - 'SendAction', - 'Series', - 'Service', - 'ServiceChannel', - 'ShareAction', - 'ShoeStore', - 'ShoppingCenter', - 'SingleFamilyResidence', - 'SiteNavigationElement', - 'SkiResort', - 'SocialEvent', - 'SocialMediaPosting', - 'SoftwareApplication', - 'SoftwareSourceCode', - 'SomeProducts', - 'SpeakableSpecification', - 'Specialty', - 'SportingGoodsStore', - 'SportsActivityLocation', - 'SportsClub', - 'SportsEvent', - 'SportsOrganization', - 'SportsTeam', - 'SpreadsheetDigitalDocument', - 'StadiumOrArena', - 'State', - 'SteeringPositionValue', - 'Store', - 'StructuredValue', - 'SubscribeAction', - 'SubwayStation', - 'Suite', - 'SuspendAction', - 'Synagogue', - 'TVClip', - 'TVEpisode', - 'TVSeason', - 'TVSeries', - 'Table', - 'TakeAction', - 'TattooParlor', - 'TaxiReservation', - 'TaxiService', - 'TaxiStand', - 'TechArticle', - 'TelevisionChannel', - 'TelevisionStation', - 'TennisComplex', - 'TextDigitalDocument', - 'TheaterEvent', - 'TheaterGroup', - 'Thing', - 'Ticket', - 'TieAction', - 'TipAction', - 'TireShop', - 'TouristAttraction', - 'TouristInformationCenter', - 'ToyStore', - 'TrackAction', - 'TradeAction', - 'TrainReservation', - 'TrainStation', - 'TrainTrip', - 'TransferAction', - 'TravelAction', - 'TravelAgency', - 'Trip', - 'TypeAndQuantityNode', - 'UnRegisterAction', - 'UnitPriceSpecification', - 'UpdateAction', - 'UseAction', - 'Vehicle', - 'VideoGallery', - 'VideoGame', - 'VideoGameClip', - 'VideoGameSeries', - 'VideoObject', - 'ViewAction', - 'VisualArtsEvent', - 'VisualArtwork', - 'Volcano', - 'VoteAction', - 'WPAdBlock', - 'WPFooter', - 'WPHeader', - 'WPSideBar', - 'WantAction', - 'WarrantyPromise', - 'WarrantyScope', - 'WatchAction', - 'Waterfall', - 'WearAction', - 'WebApplication', - 'WebPage', - 'WebPageElement', - 'WebSite', - 'WholesaleStore', - 'WinAction', - 'Winery', - 'WorkersUnion', - 'WriteAction', - 'Zoo', - ]; + return \array_keys($this->getTypesWithModels()); + } + + private function getTypesWithModels(): array + { + if (empty(static::$types)) { + static::$types = $this->loadConfiguration(); + } + + return static::$types; + } + + private function loadConfiguration(): array + { + if ($this->cache->has(static::CACHE_ENTRY_IDENTIFIER_TYPES)) { + return $this->cache->require(static::CACHE_ENTRY_IDENTIFIER_TYPES); + } + + $packages = $this->packageManager->getActivePackages(); + $allTypeModels = [[]]; + foreach ($packages as $package) { + $typeModelsConfiguration = $package->getPackagePath() . 'Configuration/TxSchema/TypeModels.php'; + if (\file_exists($typeModelsConfiguration)) { + $typeModelsInPackage = require $typeModelsConfiguration; + if (\is_array($typeModelsInPackage)) { + $allTypeModels[] = $this->enrichTypeModelsArrayWithTypeKey($typeModelsInPackage); + } + } + } + $typeModels = \array_replace_recursive(...$allTypeModels); + \ksort($typeModels); + + $this->cache->set(static::CACHE_ENTRY_IDENTIFIER_TYPES, 'return ' . \var_export($typeModels, true) . ';'); + + return $typeModels; + } + + private function enrichTypeModelsArrayWithTypeKey(array $typeModels): array + { + $typeModelsWithTypeKey = []; + foreach ($typeModels as $typeModel) { + $type = \substr(\strrchr($typeModel, '\\') ?: '', 1); + $typeModelsWithTypeKey[$type] = $typeModel; + } + + return $typeModelsWithTypeKey; } /** @@ -636,21 +114,40 @@ public function getTypes(): array */ public function getWebPageTypes(): array { - return [ - 'AboutPage', - 'CheckoutPage', - 'CollectionPage', - 'ContactPage', - 'FAQPage', - 'ImageGallery', - 'ItemPage', - 'MediaGallery', - 'ProfilePage', - 'QAPage', - 'SearchResultsPage', - 'VideoGallery', - 'WebPage', - ]; + if (empty(static::$webPageTypes)) { + static::$webPageTypes = $this->loadSpecialTypes( + static::CACHE_ENTRY_IDENTIFIER_WEBPAGE_TYPES, + WebPageTypeInterface::class + ); + } + + return static::$webPageTypes; + } + + private function loadSpecialTypes(string $cacheEntryIdentifier, string $typeInterface): array + { + if ($this->cache->has($cacheEntryIdentifier)) { + return $this->cache->require($cacheEntryIdentifier); + } + + $specialTypes = []; + foreach ($this->getTypesWithModels() as $type => $typeModel) { + try { + $interfaces = \array_keys((new \ReflectionClass($typeModel))->getInterfaces()); + + if (\in_array($typeInterface, $interfaces)) { + $specialTypes[] = $type; + } + } catch (\ReflectionException $e) { + // Ignore + } + } + + \sort($specialTypes); + + $this->cache->set($cacheEntryIdentifier, 'return ' . \var_export($specialTypes, true) . ';'); + + return $specialTypes; } /** @@ -659,15 +156,14 @@ public function getWebPageTypes(): array */ public function getWebPageElementTypes(): array { - return [ - 'SiteNavigationElement', - 'Table', - 'WPAdBlock', - 'WPFooter', - 'WPHeader', - 'WPSideBar', - 'WebPageElement', - ]; + if (empty(static::$webPageElementTypes)) { + static::$webPageElementTypes = $this->loadSpecialTypes( + static::CACHE_ENTRY_IDENTIFIER_WEBPAGEELEMENT_TYPES, + WebPageElementTypeInterface::class + ); + } + + return static::$webPageElementTypes; } /** diff --git a/Classes/Provider/WebPageTypeProvider.php b/Classes/Provider/WebPageTypeProvider.php index bc53e84e..22dc4121 100644 --- a/Classes/Provider/WebPageTypeProvider.php +++ b/Classes/Provider/WebPageTypeProvider.php @@ -15,9 +15,12 @@ */ final class WebPageTypeProvider { + /** @var TypesProvider */ + private static $typesProvider; + public static function getTypesForTcaSelect(): array { - $types = (new TypesProvider())->getWebPageTypes(); + $types = static::getTypesProvider()->getWebPageTypes(); \array_walk($types, function (&$type) { $type = [$type, $type]; @@ -25,4 +28,23 @@ public static function getTypesForTcaSelect(): array return \array_merge([['', '']], $types); } + + private static function getTypesProvider(): TypesProvider + { + if (empty(static::$typesProvider)) { + static::$typesProvider = new TypesProvider(); + } + + return static::$typesProvider; + } + + /** + * For testing purposes only! + * + * @param TypesProvider $typesProvider + */ + public static function setTypesProvider(TypesProvider $typesProvider): void + { + static::$typesProvider = $typesProvider; + } } diff --git a/Configuration/TxSchema/TypeModels.php b/Configuration/TxSchema/TypeModels.php new file mode 100644 index 00000000..f3854ac3 --- /dev/null +++ b/Configuration/TxSchema/TypeModels.php @@ -0,0 +1,604 @@ +buildGraph(); $this->attachPropertiesToTypes(); $this->identifyWebPageTypes(); + $this->identifyWebPageElementTypes(); $this->createTypes(); } @@ -196,6 +198,7 @@ protected function createType(Vertex $type): void 'className' => $label, 'properties' => $properties, 'isWebPageType' => \in_array($label, $this->webPageTypes), + 'isWebPageElementType' => \in_array($label, $this->webPageElementTypes), ] ); @@ -296,15 +299,13 @@ protected function createListOfAvailableTypes(Vertex $rootType): void \sort($types); $providerClass = $this->twig->render( - 'TypesProvider.php.twig', + 'TypeModels.php.twig', [ 'types' => $types, - 'webPageTypes' => $this->webPageTypes, - 'webPageElementTypes' => $this->identifyWebPageElementTypes(), ] ); - \file_put_contents($this->configuration->typesProviderTemplate, $providerClass); + \file_put_contents($this->configuration->typeModelsTemplate, $providerClass); } protected function collectAvailableTypes(Vertex $type): array @@ -317,12 +318,10 @@ protected function collectAvailableTypes(Vertex $type): array return $types; } - protected function identifyWebPageElementTypes(): array + protected function identifyWebPageElementTypes(): void { - $types = $this->getWebPageElementTypeChildren($this->types[static::ROOT_WEBPAGEELEMENT_TYPE_ID]); - \sort($types); - - return $types; + $this->webPageElementTypes = $this->getWebPageElementTypeChildren($this->types[static::ROOT_WEBPAGEELEMENT_TYPE_ID]); + \sort($this->webPageElementTypes); } protected function getWebPageElementTypeChildren(Vertex $type): array diff --git a/Generator/Templates/Type.php.twig b/Generator/Templates/Type.php.twig index 5371dec1..ba05d84a 100644 --- a/Generator/Templates/Type.php.twig +++ b/Generator/Templates/Type.php.twig @@ -14,6 +14,9 @@ use Brotkrueml\Schema\Core\Model\AbstractType; {% if isWebPageType %} use Brotkrueml\Schema\Core\Model\WebPageTypeInterface; {% endif %} +{% if isWebPageElementType %} + use Brotkrueml\Schema\Core\Model\WebPageElementTypeInterface; +{% endif %} /** * {{ comment }} @@ -22,6 +25,9 @@ final class {{ className }} extends AbstractType {% if isWebPageType %} implements WebPageTypeInterface {% endif %} +{% if isWebPageElementType %} + implements WebPageElementTypeInterface +{% endif %} { protected $properties = [ {% for property in properties %} diff --git a/Generator/Templates/TypeModels.php.twig b/Generator/Templates/TypeModels.php.twig new file mode 100644 index 00000000..ef5e9893 --- /dev/null +++ b/Generator/Templates/TypeModels.php.twig @@ -0,0 +1,6 @@ +getTypes(), - $this->getWebPageTypes(), - $this->getWebPageElementTypes(), - [ - 'BreadcrumbList', - 'WebSite', - ] - ) - ); - } -} diff --git a/Generator/generate.php b/Generator/generate.php index 5f8bfabe..8cec7c22 100644 --- a/Generator/generate.php +++ b/Generator/generate.php @@ -11,7 +11,7 @@ $configuration->schemaPath = __DIR__ . '/Schema/schema.jsonld'; $configuration->modelTypePathTemplate = __DIR__ . '/../Classes/Model/Type/%s.php'; $configuration->viewHelperTypePathTemplate = __DIR__ . '/../Classes/ViewHelpers/Type/%sViewHelper.php'; -$configuration->typesProviderTemplate = __DIR__ . '/../Classes/Provider/TypesProvider.php'; +$configuration->typeModelsTemplate = __DIR__ . '/../Configuration/TxSchema/TypeModels.php'; $loader = new FilesystemLoader(__DIR__ . '/Templates'); $twig = new Environment($loader, [ diff --git a/Tests/Fixtures/Configuration/TxSchema/TypeModels.php b/Tests/Fixtures/Configuration/TxSchema/TypeModels.php new file mode 100644 index 00000000..866e594f --- /dev/null +++ b/Tests/Fixtures/Configuration/TxSchema/TypeModels.php @@ -0,0 +1,9 @@ + null, diff --git a/Tests/Fixtures/Model/Type/WebSite.php b/Tests/Fixtures/Model/Type/WebSite.php new file mode 100644 index 00000000..3abd42c8 --- /dev/null +++ b/Tests/Fixtures/Model/Type/WebSite.php @@ -0,0 +1,10 @@ +cacheFrontendMock = $this->createMock(PhpFrontend::class); + + $cacheStub = $this->createStub(CacheManager::class); + $cacheStub + ->method('getCache') + ->with('tx_schema_core') + ->willReturn($this->cacheFrontendMock); + + $packageStub1 = $this->createStub(PackageInterface::class); + $packageStub1 + ->method('getPackagePath') + ->willReturn(__DIR__ . '/../../Fixtures/'); + + $packageStub2 = $this->createStub(PackageInterface::class); + $packageStub2 + ->method('getPackagePath') + ->willReturn(__DIR__ . '/NotExisting/'); + + $packageManagerStub = $this->createStub(PackageManager::class); + $packageManagerStub + ->method('getActivePackages') + ->willReturn([$packageStub1, $packageStub2]); + + $this->subject = new TypesProvider($cacheStub, $packageManagerStub); + } + + /** + * @test + */ + public function getTypesReturnsTypesFromCacheCorrectly(): void + { + $this->cacheFrontendMock + ->expects(self::once()) + ->method('has') + ->with('types') + ->willReturn(true); + + $this->cacheFrontendMock + ->expects(self::once()) + ->method('require') + ->with('types') + ->willReturn([ + 'FixtureImage' => \Brotkrueml\Schema\Tests\Fixtures\Model\Type\FixtureImage::class, + 'VideoGallery' => \Brotkrueml\Schema\Tests\Fixtures\Model\Type\VideoGallery::class, + 'WebPage' => \Brotkrueml\Schema\Tests\Fixtures\Model\Type\WebPage::class, + ]); + + $this->cacheFrontendMock + ->expects(self::never()) + ->method('set'); + + $actual = $this->subject->getTypes(); + + self::assertSame(['FixtureImage', 'VideoGallery', 'WebPage'], $actual); + } + + /** + * @test + */ + public function getTypesReturnsTypesWithReadingConfiguration(): void + { + $this->cacheFrontendMock + ->expects(self::once()) + ->method('has') + ->with('types') + ->willReturn(false); + + $this->cacheFrontendMock + ->expects(self::never()) + ->method('require'); + + $this->cacheFrontendMock + ->expects(self::once()) + ->method('set') + ->with( + 'types', + "return array ( + 'BreadcrumbList' => 'Brotkrueml\\\\Schema\\\\Tests\\\\Fixtures\\\\Model\\\\Type\\\\BreadcrumbList', + 'FixtureImage' => 'Brotkrueml\\\\Schema\\\\Tests\\\\Fixtures\\\\Model\\\\Type\\\\FixtureImage', + 'FixtureThing' => 'Brotkrueml\\\\Schema\\\\Tests\\\\Fixtures\\\\Model\\\\Type\\\\FixtureThing', + 'Table' => 'Brotkrueml\\\\Schema\\\\Tests\\\\Fixtures\\\\Model\\\\Type\\\\Table', + 'WebPage' => 'Brotkrueml\\\\Schema\\\\Tests\\\\Fixtures\\\\Model\\\\Type\\\\WebPage', + 'WebSite' => 'Brotkrueml\\\\Schema\\\\Tests\\\\Fixtures\\\\Model\\\\Type\\\\WebSite', +);" + ); + + $actual = $this->subject->getTypes(); + + self::assertSame(['BreadcrumbList', 'FixtureImage', 'FixtureThing', 'Table', 'WebPage', 'WebSite'], $actual); + } + + /** + * @test + */ + public function getTypesReturnsTypesFromClassVariableWhenCalledTheSecondTime(): void + { + $this->cacheFrontendMock + ->expects(self::once()) + ->method('has') + ->with('types') + ->willReturn(false); + + $this->subject->getTypes(); + $actual = $this->subject->getTypes(); + + self::assertSame(['BreadcrumbList', 'FixtureImage', 'FixtureThing', 'Table', 'WebPage', 'WebSite'], $actual); + } + + /** + * @test + */ + public function getWebPageTypesReturnsTypesFromCacheCorrectly(): void + { + $this->cacheFrontendMock + ->expects(self::once()) + ->method('has') + ->with('webpage_types') + ->willReturn(true); + + $this->cacheFrontendMock + ->expects(self::once()) + ->method('require') + ->with('webpage_types') + ->willReturn([ + 'VideoGallery', + 'WebPage', + ]); + + $actual = $this->subject->getWebPageTypes(); + + self::assertSame(['VideoGallery', 'WebPage'], $actual); + } + + /** + * @test + */ + public function getWebPageTypesLoadsTypesNotFromCache(): void + { + $this->cacheFrontendMock + ->method('has') + ->willReturn(false); + + $actual = $this->subject->getWebPageTypes(); + + self::assertSame(['WebPage'], $actual); + } + + /** + * @test + */ + public function getWebPageTypesReturnsTypesFromClassVariableWhenCalledTheSecondTime(): void + { + $this->cacheFrontendMock + ->expects(self::exactly(2)) // on webpage_types and types once + ->method('has') + ->willReturn(false); + + $this->subject->getWebPageTypes(); + $actual = $this->subject->getWebPageTypes(); + + self::assertSame(['WebPage'], $actual); + } + + /** + * @test + */ + public function getWebPageElementTypesReturnsTypesFromCacheCorrectly(): void + { + $this->cacheFrontendMock + ->expects(self::once()) + ->method('has') + ->with('webpageelement_types') + ->willReturn(true); + + $this->cacheFrontendMock + ->expects(self::once()) + ->method('require') + ->with('webpageelement_types') + ->willReturn([ + 'Table', + 'WebPageElement', + ]); + + $actual = $this->subject->getWebPageElementTypes(); + + self::assertSame(['Table', 'WebPageElement'], $actual); + } + + /** + * @test + */ + public function getWebPageElementTypesLoadsTypesNotFromCache(): void + { + $this->cacheFrontendMock + ->method('has') + ->willReturn(false); + + $actual = $this->subject->getWebPageElementTypes(); + + self::assertSame(['Table'], $actual); + } + + /** + * @test + */ + public function getWebPageElementTypesReturnsTypesFromClassVariableWhenCalledTheSecondTime(): void + { + $this->cacheFrontendMock + ->expects(self::exactly(2)) // on webpageelement_types and types once + ->method('has') + ->willReturn(false); + + $this->subject->getWebPageElementTypes(); + $actual = $this->subject->getWebPageElementTypes(); + + self::assertSame(['Table'], $actual); + } + + /** + * @test + */ + public function getContentTypesReturnsOnlyTheContentTypes(): void + { + $this->cacheFrontendMock + ->expects(self::exactly(3)) // on webpage_types, webpageelement_types and types once + ->method('has') + ->willReturn(false); + + $actual = $this->subject->getContentTypes(); + + self::assertSame(['FixtureImage', 'FixtureThing'], $actual); + } +} diff --git a/Tests/Unit/Provider/WebPageTypeProviderTest.php b/Tests/Unit/Provider/WebPageTypeProviderTest.php index 495f83e5..df410403 100644 --- a/Tests/Unit/Provider/WebPageTypeProviderTest.php +++ b/Tests/Unit/Provider/WebPageTypeProviderTest.php @@ -2,78 +2,42 @@ namespace Brotkrueml\Schema\Tests\Unit\Provider; -use Brotkrueml\Schema\Core\Model\WebPageTypeInterface; +use Brotkrueml\Schema\Provider\TypesProvider; use Brotkrueml\Schema\Provider\WebPageTypeProvider; -use Brotkrueml\Schema\Tests\Helper\SchemaCacheTrait; -use Brotkrueml\Schema\Utility\Utility; use PHPUnit\Framework\TestCase; -use TYPO3\CMS\Core\Utility\GeneralUtility; class WebPageTypeProviderTest extends TestCase { - use SchemaCacheTrait; + protected $availableWebPageTypesForTesting = [ + 'FooPage', + 'BarPage', + 'SomePage', + 'AnotherPage', + ]; protected function setUp(): void { - $this->defineCacheStubsWhichReturnEmptyEntry(); - } + $typesProviderStub = $this->createStub(TypesProvider::class); + $typesProviderStub + ->method('getWebPageTypes') + ->willReturn($this->availableWebPageTypesForTesting); - protected function tearDown(): void - { - GeneralUtility::purgeInstances(); + WebPageTypeProvider::setTypesProvider($typesProviderStub); } - public function dataProvider(): iterable { - $webPageTypes = [ - 'AboutPage', - 'CheckoutPage', - 'CollectionPage', - 'ContactPage', - 'FAQPage', - 'ImageGallery', - 'ItemPage', - 'MediaGallery', - 'ProfilePage', - 'QAPage', - 'SearchResultsPage', - 'VideoGallery', - 'WebPage', - ]; - - foreach ($webPageTypes as $type) { + foreach ($this->availableWebPageTypesForTesting as $type) { yield \sprintf('Type "%s"', $type) => [$type]; } } /** - * We have to assure that no WebPage type is removed by the generator - * when the schema definition changes. A WebPage type can be assigned - * by the user in the page field! - * - * @test - * @dataProvider dataProvider - * - * @param string $type - */ - public function givenWebPageTypeIsAnInstanceOfWebPageTypeInterface(string $type): void - { - $className = Utility::getNamespacedClassNameForType($type); - $class = new $className(); - - self::assertInstanceOf(WebPageTypeInterface::class, $class); - } - - /** - * We also have to assure that the structure for the TCA is correct - * and has also the empty option available! - * * @test * @dataProvider dataProvider * * @param string $type */ - public function givenTypeIsInTcaSelect(string $type): void + public function getTypesForTcaSelectReturnsAllAvailableWebPageTypes(string $type): void { $actual = WebPageTypeProvider::getTypesForTcaSelect(); diff --git a/ext_localconf.php b/ext_localconf.php index 18ad36e9..bf131ae3 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,5 +1,5 @@ \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend::class, + 'backend' => \TYPO3\CMS\Core\Cache\Backend\SimpleFileBackend::class, + 'options' => [ + 'defaultLifetime' => 0, + ], + ]; + } + if (\TYPO3\CMS\Core\Utility\VersionNumberUtility::convertVersionNumberToInteger(TYPO3_branch) < 10000000) { $signalSlotDispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance( TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class