홀리스 크릭 가족 치과 · 수리 보고서

증거물 B-3 근거: 커밋 031d47e

커밋 시각 · 031d47ee032e9d97b0eb69182271689986bca24a

보험 안내 페이지가 흰 화면이었습니다

저장소 자체는 비공개입니다. 이 페이지에 커밋을 기록된 그대로 옮겼습니다.

커밋 메시지

보험 페이지 흰 화면 수정(PHP 8에서 삭제된 create_function)

Fix B-3: Insurance page white screen (create_function removed in PHP 8)

Root cause: HC Insurance Table sorted the plans with create_function(),
removed in PHP 8.0. The sort ran on the `wp` hook for the Insurance page,
before any output, so PHP 8.3 stopped with "Call to undefined function
create_function()" and WordPress answered HTTP 500 with only "There has
been a critical error on this website."

Fix: sort with a closure and drop the pre-render hook; the shortcode
sorts when it renders.

Proof: tests/phpunit/repair/InsuranceTableTest.php,
e2e/tests/b-php8.spec.ts.

Exhibit: B-3

변경 내용

git에 기록된 텍스트 파일만 보여 줍니다. +로 시작하는 줄은 추가, −로 시작하는 줄은 삭제입니다.

바뀐 파일

---
 e2e/tests/b-php8.spec.ts                                        | 19 +++++++++
 .../plugins/hc-insurance-table/hc-insurance-table.php           | 17 +++-----
 tests/phpunit/repair/InsuranceTableTest.php                     | 61 +++++++++++++++++++++++++++++
 3 files changed, 86 insertions(+), 11 deletions(-)
diff --git a/e2e/tests/b-php8.spec.ts b/e2e/tests/b-php8.spec.ts
index 9971548..a8ba2a5 100644
--- a/e2e/tests/b-php8.spec.ts
+++ b/e2e/tests/b-php8.spec.ts
@@ -3,6 +3,25 @@ import { BEFORE, AFTER } from './helpers';
 
 // Exhibit B: PHP 8 errors as a visitor meets them.
 
+test('B-3 before: the Insurance page is a white screen with a fatal error', async ({ page }) => {
+  const res = await page.goto(BEFORE + '/insurance/');
+  expect(res!.status()).toBe(500);
+  await expect(page.locator('body')).toContainText('There has been a critical error on this website.');
+  await expect(page.getByRole('heading', { name: 'Insurance and payment' })).toHaveCount(0);
+  await expect(page.getByRole('table')).toHaveCount(0);
+});
+
+test('B-3 after: the Insurance page shows the plans, sorted by name', async ({ page }) => {
+  const res = await page.goto(AFTER + '/insurance/');
+  expect(res!.status()).toBe(200);
+  await expect(page.getByRole('heading', { level: 1, name: 'Insurance and payment' })).toBeVisible();
+  const table = page.getByRole('table', { name: 'Insurance we work with' });
+  const plans = await table.locator('tbody tr td:first-child').allTextContents();
+  expect(plans.length).toBe(7);
+  expect(plans).toEqual([...plans].sort((a, b) => a.localeCompare(b)));
+  expect(plans[0]).toBe('Alder County Employees Plan');
+});
+
 test('B-1 before: PHP deprecation notices are printed above the page and the log is public', async ({ page, request }) => {
   await page.goto(BEFORE + '/');
   await expect(page.locator('body')).toContainText('Deprecated:');
diff --git a/site/wp-content/plugins/hc-insurance-table/hc-insurance-table.php b/site/wp-content/plugins/hc-insurance-table/hc-insurance-table.php
index 80c8a78..bd28722 100644
--- a/site/wp-content/plugins/hc-insurance-table/hc-insurance-table.php
+++ b/site/wp-content/plugins/hc-insurance-table/hc-insurance-table.php
@@ -38,20 +38,15 @@ function hc_insurance_table_plans() {
 			}
 		)
 	);
-	usort( $plans, create_function( '$a,$b', 'return strcasecmp( $a["name"], $b["name"] );' ) );
+	usort(
+		$plans,
+		static function ( $a, $b ) {
+			return strcasecmp( $a['name'], $b['name'] );
+		}
+	);
 	return $plans;
 }
 
-/**
- * Prepares the sorted plans before the Insurance page renders (cached for the shortcode).
- */
-function hc_insurance_table_prepare() {
-	if ( is_page( 'insurance' ) ) {
-		$GLOBALS['hc_insurance_table_cache'] = hc_insurance_table_plans();
-	}
-}
-add_action( 'wp', 'hc_insurance_table_prepare' );
-
 /**
  * Label for a network status.
  *
diff --git a/tests/phpunit/repair/InsuranceTableTest.php b/tests/phpunit/repair/InsuranceTableTest.php
new file mode 100644
index 0000000..5b4f92b
--- /dev/null
+++ b/tests/phpunit/repair/InsuranceTableTest.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Exhibit B-3: the Insurance page renders on PHP 8 (no create_function()).
+ *
+ * @package Hollis\Tests
+ */
+
+/**
+ * Insurance table.
+ */
+class InsuranceTableTest extends WP_UnitTestCase {
+
+	public function set_up() {
+		parent::set_up();
+		update_option(
+			'hcit_plans',
+			array(
+				array(
+					'name'    => 'Zephyr Mutual',
+					'type'    => 'PPO',
+					'network' => 'in',
+				),
+				array(
+					'name'    => 'alder County Plan',
+					'type'    => 'Indemnity',
+					'network' => 'out',
+				),
+				array( 'name' => '' ),
+				array(
+					'name'    => 'Brightline',
+					'type'    => 'HMO',
+					'network' => 'in',
+				),
+			)
+		);
+	}
+
+	public function test_no_removed_php_functions_remain() {
+		$code = file_get_contents( HOLLIS_CODE . '/plugins/hc-insurance-table/hc-insurance-table.php' );
+		foreach ( array( 'create_function', 'each(', 'money_format', 'ereg' ) as $removed ) {
+			$this->assertStringNotContainsString( $removed, $code );
+		}
+	}
+
+	public function test_insurance_page_renders_the_table_sorted_case_insensitively() {
+		$page = self::factory()->post->create(
+			array(
+				'post_type'    => 'page',
+				'post_name'    => 'insurance',
+				'post_content' => '[hc_insurance_table]',
+			)
+		);
+		$this->go_to( get_permalink( $page ) );
+		do_action( 'wp' );
+		$html = do_shortcode( '[hc_insurance_table]' );
+		preg_match_all( '#<tr><td>([^<]+)</td>#', $html, $m );
+		$this->assertSame( array( 'alder County Plan', 'Brightline', 'Zephyr Mutual' ), $m[1] );
+		$this->assertStringContainsString( '<caption>Insurance we work with</caption>', $html );
+		$this->assertStringContainsString( 'Out of network: we file the claim for you', $html );
+	}
+}

증명 테스트

tests/phpunit/repair/InsuranceTableTest.php

tests/phpunit/repair/InsuranceTableTest.php, 커밋된 그대로.

<?php
/**
 * Exhibit B-3: the Insurance page renders on PHP 8 (no create_function()).
 *
 * @package Hollis\Tests
 */

/**
 * Insurance table.
 */
class InsuranceTableTest extends WP_UnitTestCase {

	public function set_up() {
		parent::set_up();
		update_option(
			'hcit_plans',
			array(
				array(
					'name'    => 'Zephyr Mutual',
					'type'    => 'PPO',
					'network' => 'in',
				),
				array(
					'name'    => 'alder County Plan',
					'type'    => 'Indemnity',
					'network' => 'out',
				),
				array( 'name' => '' ),
				array(
					'name'    => 'Brightline',
					'type'    => 'HMO',
					'network' => 'in',
				),
			)
		);
	}

	public function test_no_removed_php_functions_remain() {
		$code = file_get_contents( HOLLIS_CODE . '/plugins/hc-insurance-table/hc-insurance-table.php' );
		foreach ( array( 'create_function', 'each(', 'money_format', 'ereg' ) as $removed ) {
			$this->assertStringNotContainsString( $removed, $code );
		}
	}

	public function test_insurance_page_renders_the_table_sorted_case_insensitively() {
		$page = self::factory()->post->create(
			array(
				'post_type'    => 'page',
				'post_name'    => 'insurance',
				'post_content' => '[hc_insurance_table]',
			)
		);
		$this->go_to( get_permalink( $page ) );
		do_action( 'wp' );
		$html = do_shortcode( '[hc_insurance_table]' );
		preg_match_all( '#<tr><td>([^<]+)</td>#', $html, $m );
		$this->assertSame( array( 'alder County Plan', 'Brightline', 'Zephyr Mutual' ), $m[1] );
		$this->assertStringContainsString( '<caption>Insurance we work with</caption>', $html );
		$this->assertStringContainsString( 'Out of network: we file the claim for you', $html );
	}
}

e2e/tests/b-php8.spec.ts

e2e/tests/b-php8.spec.ts, 커밋된 그대로.

import { test, expect } from '@playwright/test';
import { BEFORE, AFTER } from './helpers';

// Exhibit B: PHP 8 errors as a visitor meets them.

test('B-3 before: the Insurance page is a white screen with a fatal error', async ({ page }) => {
  const res = await page.goto(BEFORE + '/insurance/');
  expect(res!.status()).toBe(500);
  await expect(page.locator('body')).toContainText('There has been a critical error on this website.');
  await expect(page.getByRole('heading', { name: 'Insurance and payment' })).toHaveCount(0);
  await expect(page.getByRole('table')).toHaveCount(0);
});

test('B-3 after: the Insurance page shows the plans, sorted by name', async ({ page }) => {
  const res = await page.goto(AFTER + '/insurance/');
  expect(res!.status()).toBe(200);
  await expect(page.getByRole('heading', { level: 1, name: 'Insurance and payment' })).toBeVisible();
  const table = page.getByRole('table', { name: 'Insurance we work with' });
  const plans = await table.locator('tbody tr td:first-child').allTextContents();
  expect(plans.length).toBe(7);
  expect(plans).toEqual([...plans].sort((a, b) => a.localeCompare(b)));
  expect(plans[0]).toBe('Alder County Employees Plan');
});

test('B-1 before: PHP deprecation notices are printed above the page and the log is public', async ({ page, request }) => {
  await page.goto(BEFORE + '/');
  await expect(page.locator('body')).toContainText('Deprecated:');
  const log = await request.get(BEFORE + '/wp-content/debug.log');
  expect(log.status()).toBe(200);
  expect(await log.text()).toContain('PHP Deprecated');
});

test('B-1 after: no notice on any page and no public debug log', async ({ page, request }) => {
  for (const path of ['/', '/our-team/', '/services/', '/request-appointment/', '/news/']) {
    await page.goto(AFTER + path);
    await expect(page.locator('body')).not.toContainText(/Deprecated:|Warning:|Notice:/);
  }
  const log = await request.get(AFTER + '/wp-content/debug.log');
  expect(log.status()).toBe(404);
});

증거물 B-3로 돌아가기