*/ public function app(): array { return array( 'order' => 20, 'category' => 'support', 'core' => false, 'deps' => array( ), 'entities' => array( 'kb_article' ), 'default' => true, 'label_key' => 'app_knowledge_base', 'desc_key' => 'app_knowledge_base_desc', 'icon' => 'book', 'cron' => array( ), ); } /** * @return array{label: string, desc: string} */ public function labels(): array { return array( 'label' => __( 'Knowledge base', 'plugixa-crm' ), 'desc' => __( 'Articles for your team, and a public help centre if you want one.', 'plugixa-crm' ), ); } public function migrate(): void { Schema::install(); // The categories predate the closure table by one release, so their paths have to be derived // from the `parent_id` column they already have. Without this every descendant query answers // "nothing" on exactly the installs that have categories worth querying. Idempotent. ( new CategoryRepository() )->ensure_paths(); } /** * @return array */ public function capabilities(): array { return array(); } public function registerServices( Container $container ): void { unset( $container ); } public function registerRoutes( Container $container ): void { $container->get( ArticlesController::class )->register_routes(); } public function registerHooks( Container $container ): void { add_filter( 'plugixa_crm_admin_i18n_strings', static fn( array $strings ): array => array_merge( $strings, Strings::all() ) ); add_filter( 'plugixa_crm_entity_types', array( $this, 'register_entities' ) ); ( new Settings() )->register_hooks(); // The public help centre. Its assets register on `wp_enqueue_scripts` and enqueue only where // the shortcode actually renders, so a site that never places it pays nothing. ( new KbShortcode( new KbRenderer( new PublicArticles() ) ) )->register_hooks(); // Generic write access, so CSV import reaches the SAME service the REST API does — and // therefore the same validation, the same derived columns and the same lifecycle events. add_filter( 'plugixa_crm_record_writers', static function ( array $writers ) use ( $container ): array { $writers['kb_article'] = array( 'create' => array( $container->get( ArticleService::class ), 'create' ), 'update' => array( $container->get( ArticleService::class ), 'update' ), 'delete' => array( $container->get( ArticleRepository::class ), 'delete' ), ); return $writers; } ); add_filter( 'plugixa_crm_record_names', static function ( array $names, string $entity_type, array $ids ) use ( $container ): array { return 'kb_article' === $entity_type ? $container->get( ArticleRepository::class )->names( $ids ) : $names; }, 10, 3 ); add_filter( 'plugixa_crm_search_providers', static function ( array $providers ) use ( $container ): array { $providers['kb_article'] = array( 'label' => __( 'Knowledge base', 'plugixa-crm' ), 'route' => '/kb-articles/{id}/edit', 'order' => 60, 'search' => array( $container->get( ArticleRepository::class ), 'search' ), ); return $providers; } ); } /** * What must keep listening while this app is switched off — its half of purge, merge, erasure * and uninstall. See LifecycleInterface. * * @param Container $container The container. */ public function registerLifecycle( Container $container ): void { // The public help centre renders nothing while the app is off, rather than its shortcode as text. DormantShortcode::keep( KbShortcode::TAG ); // Merging two cases brings the duplicate's linked articles; this module owns the table, as below. add_filter( 'plugixa_crm_merge_references', static function ( array $references, string $entity_type ): array { if ( 'case' === $entity_type ) { $references[] = array( 'table' => 'case_articles', 'id_column' => 'case_id', ); } return $references; }, 10, 2 ); // Links go when either end is purged. An article's links go with the article; a purged CASE // takes its article links too — and that listener lives here rather than in Cases, because // this module owns the table and Cases must not know it exists. add_action( 'plugixa_crm_before_purge', static function ( string $entity_type, int $id ) use ( $container ): void { $links = $container->get( ArticleLinkRepository::class ); if ( 'kb_article' === $entity_type ) { $links->wipe_for_article( $id ); } elseif ( 'case' === $entity_type ) { $links->wipe_for_case( $id ); } }, 10, 2 ); } /** * @param array $entities Whatever other modules registered. * * @return array */ public function register_entities( array $entities ): array { $statuses = array(); foreach ( ArticleService::STATUSES as $status ) { $statuses[ $status ] = ucwords( str_replace( '_', ' ', $status ) ); } $entities['kb_article'] = array( 'label' => __( 'Article', 'plugixa-crm' ), 'plural' => __( 'Knowledge base', 'plugixa-crm' ), 'api_base' => 'kb_articles', 'fields' => array( 'name' => array( 'label' => __( 'Title', 'plugixa-crm' ), 'type' => 'text', ), 'status' => array( 'label' => __( 'Status', 'plugixa-crm' ), 'type' => 'select', 'options' => $statuses, ), 'language' => array( 'label' => __( 'Language', 'plugixa-crm' ), 'type' => 'text', ), 'publish_date' => array( 'label' => __( 'Publish date', 'plugixa-crm' ), 'type' => 'date', ), 'expiration_date' => array( 'label' => __( 'Expires', 'plugixa-crm' ), 'type' => 'date', ), ), ); return $entities; } }