Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
Local_Business_Schema_Node
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
3 / 3
32
100.00% covered (success)
100.00%
1 / 1
 extend
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
17
 postal_address
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 opening_hours_specification
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
10
1<?php
2/**
3 * LocalBusiness Schema.org decorator for the site-level Organization node.
4 *
5 * Adds LocalBusiness properties to the existing Organization entity when the
6 * admin has enabled local-business schema and configured an address. This keeps
7 * all publisher references pointing at the same stable `#organization` node
8 * instead of introducing a second business entity.
9 *
10 * @package automattic/jetpack-seo-package
11 */
12
13namespace Automattic\Jetpack\SEO;
14
15/**
16 * Decorates the site-level Organization node with LocalBusiness details.
17 */
18class Local_Business_Schema_Node {
19
20    /**
21     * Schema.org opening-hours day-code order and emitted day names.
22     *
23     * @var array<string, string>
24     */
25    const DAY_NAMES = array(
26        'Mo' => 'Monday',
27        'Tu' => 'Tuesday',
28        'We' => 'Wednesday',
29        'Th' => 'Thursday',
30        'Fr' => 'Friday',
31        'Sa' => 'Saturday',
32        'Su' => 'Sunday',
33    );
34
35    /**
36     * Extend an Organization node with LocalBusiness properties.
37     *
38     * @param array|null $organization Built Organization node (null passes through).
39     * @param array      $settings     Effective values from Schema_Settings::get_local_business().
40     * @return array|null
41     */
42    public static function extend( $organization, array $settings ) {
43        if ( null === $organization || ! is_array( $organization ) ) {
44            return $organization;
45        }
46
47        if ( empty( $settings['enabled'] ) ) {
48            return $organization;
49        }
50
51        $address = self::postal_address( isset( $settings['address'] ) && is_array( $settings['address'] ) ? $settings['address'] : array() );
52        if ( null === $address ) {
53            return $organization;
54        }
55
56        $organization['@type']   = array( 'Organization', 'LocalBusiness' );
57        $organization['address'] = $address;
58
59        foreach ( array( 'telephone', 'priceRange' ) as $field ) {
60            if ( ! empty( $settings[ $field ] ) && is_string( $settings[ $field ] ) ) {
61                $organization[ $field ] = $settings[ $field ];
62            }
63        }
64
65        if ( isset( $settings['geo'] ) && is_array( $settings['geo'] ) ) {
66            $latitude  = $settings['geo']['latitude'] ?? '';
67            $longitude = $settings['geo']['longitude'] ?? '';
68            if ( is_numeric( $latitude ) && is_numeric( $longitude ) ) {
69                $organization['geo'] = array(
70                    '@type'     => 'GeoCoordinates',
71                    'latitude'  => (float) $latitude,
72                    'longitude' => (float) $longitude,
73                );
74            }
75        }
76
77        $hours = self::opening_hours_specification( isset( $settings['openingHours'] ) && is_array( $settings['openingHours'] ) ? $settings['openingHours'] : array() );
78        if ( ! empty( $hours ) ) {
79            $organization['openingHoursSpecification'] = $hours;
80        }
81
82        return $organization;
83    }
84
85    /**
86     * Build a PostalAddress from non-empty address subfields.
87     *
88     * @param array $address Address settings.
89     * @return array|null
90     */
91    private static function postal_address( array $address ) {
92        $postal_address = array( '@type' => 'PostalAddress' );
93        foreach ( array( 'streetAddress', 'addressLocality', 'addressRegion', 'postalCode', 'addressCountry' ) as $field ) {
94            if ( ! empty( $address[ $field ] ) && is_string( $address[ $field ] ) ) {
95                $postal_address[ $field ] = $address[ $field ];
96            }
97        }
98
99        return count( $postal_address ) > 1 ? $postal_address : null;
100    }
101
102    /**
103     * Build OpeningHoursSpecification rows for fully configured days.
104     *
105     * @param array $opening_hours Opening-hours settings keyed by schema.org day code.
106     * @return array<int, array<string, string>>
107     */
108    private static function opening_hours_specification( array $opening_hours ) {
109        $specification = array();
110        foreach ( self::DAY_NAMES as $code => $day_name ) {
111            $entry  = isset( $opening_hours[ $code ] ) && is_array( $opening_hours[ $code ] ) ? $opening_hours[ $code ] : array();
112            $opens  = isset( $entry['opens'] ) && is_string( $entry['opens'] ) ? $entry['opens'] : '';
113            $closes = isset( $entry['closes'] ) && is_string( $entry['closes'] ) ? $entry['closes'] : '';
114
115            if ( '' === $opens || '' === $closes ) {
116                continue;
117            }
118
119            $specification[] = array(
120                '@type'     => 'OpeningHoursSpecification',
121                'dayOfWeek' => $day_name,
122                'opens'     => $opens,
123                'closes'    => $closes,
124            );
125        }
126
127        return $specification;
128    }
129}