customer-riverside/web/modules/custom/riverside_pt/riverside_pt.install
Philip Peterson 1e3ba132a4 Suppress duplicate key errors on semaphore table during rebuilds
- In _riverside_pt_rebuild(): proactively TRUNCATE the semaphore table
  at the very start of every rebuild. This eliminates the common
  'duplicate key value violates unique constraint "semaphore____pkey"'
  errors for 'state:Drupal\Core\Cache\CacheCollector' and 'cron' that
  appear in postgres logs.

- In entrypoint.sh: add TRUNCATE semaphore at strategic points
  (right after site:install, before module enables, before/after
  riverside:rebuild, before final drush cr). Wrapped with || true
  so they never break the startup script.

- Added a note in CLAUDE.md under the rebuild section explaining
  the errors and the quick manual fix.

These are harmless (Drupal's DbLockBackend usually recovers) but
very noisy in the container logs during the default full rebuild
path.
2026-06-04 00:06:27 -07:00

263 lines
8.9 KiB
Text

<?php
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\path_alias\Entity\PathAlias;
use Drupal\user\Entity\Role;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
/**
* Implements hook_install().
*
* Delegates to the rebuild function so that normal module installs and
* container rebuilds both produce the same result.
*/
function riverside_pt_install() {
_riverside_pt_rebuild();
}
/**
* Rebuilds the Riverside PT site structure from code.
*
* This function is idempotent and can be safely called multiple times.
* It is the single source of truth for content types, fields, roles,
* and navigation used by the Docker entrypoint.
*/
function _riverside_pt_rebuild(): void {
// Clear any stale semaphore locks. This prevents duplicate key violations
// on the "semaphore" table (e.g. "state:Drupal\Core\Cache\CacheCollector",
// "cron") during rebuilds. These occur because rapid entity/config
// changes trigger cache collectors and lock acquisitions concurrently.
// Safe to run even on initial install (table may not exist yet).
try {
\Drupal::database()->truncate('semaphore')->execute();
}
catch (\Exception $e) {
// Ignore if table doesn't exist yet.
}
$entity_type_manager = \Drupal::entityTypeManager();
$storage_node_type = $entity_type_manager->getStorage('node_type');
$storage_role = $entity_type_manager->getStorage('user_role');
$storage_field_storage = $entity_type_manager->getStorage('field_storage_config');
$storage_field_config = $entity_type_manager->getStorage('field_config');
// --- Content types (idempotent) ---
if (!$storage_node_type->load('appointment')) {
NodeType::create([
'type' => 'appointment',
'name' => 'Appointment',
'description' => 'A booking between a Patient and a Provider at a particular time.',
'new_revision' => FALSE,
'display_submitted' => FALSE,
])->save();
}
if (!$storage_node_type->load('provider_availability')) {
NodeType::create([
'type' => 'provider_availability',
'name' => 'Provider Availability',
'description' => 'A window of time during which a Provider is available for appointments.',
'new_revision' => FALSE,
'display_submitted' => FALSE,
])->save();
}
// --- Role (idempotent) ---
if (!$storage_role->load('provider')) {
Role::create(['id' => 'provider', 'label' => 'Provider'])->save();
}
// --- Field storages (idempotent) ---
$field_storages = [
'field_appointment_date' => [
'entity_type' => 'node',
'type' => 'datetime',
'settings' => ['datetime_type' => 'datetime'],
],
'field_duration_minutes' => [
'entity_type' => 'node',
'type' => 'integer',
],
'field_service_type' => [
'entity_type' => 'node',
'type' => 'list_string',
'settings' => [
'allowed_values' => [
'diagnostic' => 'Diagnostic',
'sports_rehab' => 'Sports Rehab',
'pre_post_surgical_rehab' => 'Pre/Post-Surgical Rehab',
'neurological_pt' => 'Neurological PT',
],
],
],
'field_provider' => [
'entity_type' => 'node',
'type' => 'entity_reference',
'settings' => ['target_type' => 'user'],
],
'field_start_datetime' => [
'entity_type' => 'node',
'type' => 'datetime',
'settings' => ['datetime_type' => 'datetime'],
],
'field_end_datetime' => [
'entity_type' => 'node',
'type' => 'datetime',
'settings' => ['datetime_type' => 'datetime'],
],
];
foreach ($field_storages as $field_name => $definition) {
if (!$storage_field_storage->load("node.$field_name")) {
FieldStorageConfig::create([
'field_name' => $field_name,
] + $definition)->save();
}
}
// Clear field definition cache so FieldConfig creation can see the storages.
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
// --- Field configs (idempotent) ---
$field_configs = [
// Appointment bundle
'node.appointment.field_appointment_date' => [
'field_name' => 'field_appointment_date',
'entity_type' => 'node',
'bundle' => 'appointment',
'label' => 'Appointment Date',
'required' => TRUE,
],
'node.appointment.field_duration_minutes' => [
'field_name' => 'field_duration_minutes',
'entity_type' => 'node',
'bundle' => 'appointment',
'label' => 'Duration (Minutes)',
'required' => TRUE,
],
'node.appointment.field_service_type' => [
'field_name' => 'field_service_type',
'entity_type' => 'node',
'bundle' => 'appointment',
'label' => 'Service Type',
'required' => TRUE,
],
'node.appointment.field_provider' => [
'field_name' => 'field_provider',
'entity_type' => 'node',
'bundle' => 'appointment',
'label' => 'Provider',
'required' => TRUE,
'settings' => [
'handler' => 'default:user',
'handler_settings' => [
'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']],
],
],
],
// Provider availability bundle
'node.provider_availability.field_provider' => [
'field_name' => 'field_provider',
'entity_type' => 'node',
'bundle' => 'provider_availability',
'label' => 'Provider',
'required' => TRUE,
'settings' => [
'handler' => 'default:user',
'handler_settings' => [
'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']],
],
],
],
'node.provider_availability.field_start_datetime' => [
'field_name' => 'field_start_datetime',
'entity_type' => 'node',
'bundle' => 'provider_availability',
'label' => 'Start',
'required' => TRUE,
],
'node.provider_availability.field_end_datetime' => [
'field_name' => 'field_end_datetime',
'entity_type' => 'node',
'bundle' => 'provider_availability',
'label' => 'End',
'required' => TRUE,
],
];
foreach ($field_configs as $field_id => $definition) {
if (!$storage_field_config->load($field_id)) {
FieldConfig::create($definition)->save();
}
}
// Navigation and basic site settings (these are intentionally destructive on menu).
try {
_riverside_pt_build_navigation();
}
catch (\Exception $e) {
\Drupal::logger('riverside_pt')->error('Navigation setup failed: @msg', ['@msg' => $e->getMessage()]);
}
\Drupal::configFactory()->getEditable('system.site')
->set('name', 'Riverside Therapeutics')
->set('page.front', '/home')
->save();
}
function _riverside_pt_build_navigation(): void {
$em = \Drupal::entityTypeManager();
foreach ($em->getStorage('menu_link_content')->loadByProperties(['menu_name' => 'main']) as $link) {
$link->delete();
}
// Clean up legacy nodes for pages we now serve via custom controllers/routes
foreach (['About', 'Contact'] as $title) {
$nodes = $em->getStorage('node')->loadByProperties(['title' => $title, 'type' => 'page']);
foreach ($nodes as $node) {
$node->delete();
}
$aliases = $em->getStorage('path_alias')->loadByProperties(['alias' => '/' . strtolower($title)]);
foreach ($aliases as $alias) {
$alias->delete();
}
}
foreach (['FAQ'] as $title) {
if ($em->getStorage('node')->loadByProperties(['title' => $title, 'type' => 'page'])) {
continue;
}
$node = Node::create(['type' => 'page', 'title' => $title, 'status' => 1]);
$node->save();
PathAlias::create([
'path' => '/node/' . $node->id(),
'alias' => '/' . strtolower($title),
'langcode' => 'en',
])->save();
}
$defs = [
['title' => 'Home', 'uri' => 'route:<front>', 'weight' => 0, 'class' => NULL],
['title' => 'Services', 'uri' => 'internal:/services', 'weight' => 1, 'class' => NULL],
['title' => 'About', 'uri' => 'internal:/about', 'weight' => 2, 'class' => NULL],
['title' => 'FAQ', 'uri' => 'internal:/faq', 'weight' => 3, 'class' => NULL],
['title' => 'Contact', 'uri' => 'internal:/contact', 'weight' => 4, 'class' => 'nav-cta nav-cta--primary'],
['title' => 'Book An Appointment', 'uri' => 'internal:/home#book-an-appointment','weight' => 5, 'class' => 'nav-cta nav-cta--primary'],
];
foreach ($defs as $def) {
$options = $def['class'] ? ['attributes' => ['class' => explode(' ', $def['class'])]] : [];
MenuLinkContent::create([
'title' => $def['title'],
'link' => ['uri' => $def['uri'], 'options' => $options],
'menu_name' => 'main',
'weight' => $def['weight'],
'enabled' => TRUE,
])->save();
}
}