$data Location data. * @return Location * @throws DomainException When validation fails, or when every named location field is occupied. */ public function create( array $data ): Location { $now = $this->clock->now(); $name = trim( (string) ( $data['name'] ?? '' ) ); $slug = trim( (string) ( $data['slug'] ?? '' ) ); if ( '' === $slug ) { $slug = $this->generate_slug( $name ); } $location = new Location( null, Uuid::generate(), $name, $slug, $this->optional_string( $data['address_line1'] ?? null ), $this->optional_string( $data['address_line2'] ?? null ), $this->optional_string( $data['city'] ?? null ), $this->optional_string( $data['region'] ?? null ), $this->optional_string( $data['postcode'] ?? null ), $this->optional_country( $data['country'] ?? null ), $this->optional_string( $data['latitude'] ?? null ), $this->optional_string( $data['longitude'] ?? null ), new TimeZoneId( trim( (string) ( $data['timezone'] ?? 'Europe/London' ) ) ), $this->optional_phone( $data['phone'] ?? null ), $this->optional_email( $data['email'] ?? null ), isset( $data['status'] ) ? LocationStatus::from( (string) $data['status'] ) : LocationStatus::Active, $this->optional_colour( $data['colour'] ?? null ), null, $now, $now, ); $this->locations->save( $location ); return $location; } /** * Update an existing location. * * @param int $id Location id. * @param array $data Location data. * @return Location * @throws DomainException When the location is missing. */ public function update( int $id, array $data ): Location { $location = $this->locations->find_by_id( $id ); if ( null === $location ) { throw new DomainException( 'location.not_found', 'Location not found.' ); } $now = $this->clock->now(); $updated = new Location( $location->id(), $location->uuid(), array_key_exists( 'name', $data ) ? trim( (string) $data['name'] ) : $location->name(), array_key_exists( 'slug', $data ) ? trim( (string) $data['slug'] ) : $location->slug(), array_key_exists( 'address_line1', $data ) ? $this->optional_string( $data['address_line1'] ) : $location->address_line1(), array_key_exists( 'address_line2', $data ) ? $this->optional_string( $data['address_line2'] ) : $location->address_line2(), array_key_exists( 'city', $data ) ? $this->optional_string( $data['city'] ) : $location->city(), array_key_exists( 'region', $data ) ? $this->optional_string( $data['region'] ) : $location->region(), array_key_exists( 'postcode', $data ) ? $this->optional_string( $data['postcode'] ) : $location->postcode(), array_key_exists( 'country', $data ) ? $this->optional_country( $data['country'] ) : $location->country(), array_key_exists( 'latitude', $data ) ? $this->optional_string( $data['latitude'] ) : $location->latitude(), array_key_exists( 'longitude', $data ) ? $this->optional_string( $data['longitude'] ) : $location->longitude(), array_key_exists( 'timezone', $data ) ? new TimeZoneId( trim( (string) $data['timezone'] ) ) : $location->timezone(), array_key_exists( 'phone', $data ) ? $this->optional_phone( $data['phone'] ) : $location->phone(), array_key_exists( 'email', $data ) ? $this->optional_email( $data['email'] ) : $location->email(), array_key_exists( 'status', $data ) ? LocationStatus::from( (string) $data['status'] ) : $location->status(), array_key_exists( 'colour', $data ) ? $this->optional_colour( $data['colour'] ) : $location->colour(), $location->meta(), $location->created_at(), $now, $location->deleted_at(), ); $this->locations->save( $updated ); return $updated; } /** * Delete a location (soft delete). * * @param int $id Location id. * * @throws DomainException When the location is missing. */ public function delete( int $id ): void { $location = $this->locations->find_by_id( $id ); if ( null === $location ) { throw new DomainException( 'location.not_found', 'Location not found.' ); } $this->locations->soft_delete( $location, $this->clock->now() ); } /** * Build a unique-enough slug from the location name. * * @param string $name Display name. */ private function generate_slug( string $name ): string { $slug = strtolower( trim( $name ) ); $slug = (string) preg_replace( '/[^a-z0-9]+/', '-', $slug ); $slug = trim( $slug, '-' ); if ( '' === $slug ) { return 'location-' . substr( bin2hex( random_bytes( 4 ) ), 0, 8 ); } return $slug; } /** * Trim a mixed value to null when empty. * * @param mixed $value Incoming field. */ private function optional_string( mixed $value ): ?string { if ( null === $value ) { return null; } $raw = trim( (string) $value ); return '' === $raw ? null : $raw; } /** * ISO 3166-1 alpha-2, or null. * * @param mixed $value Incoming field. */ private function optional_country( mixed $value ): ?string { $raw = $this->optional_string( $value ); return null === $raw ? null : strtoupper( $raw ); } /** * Hex colour, or null when missing or invalid. * * @param mixed $value Incoming field. */ private function optional_colour( mixed $value ): ?string { $raw = $this->optional_string( $value ); if ( null === $raw || 1 !== preg_match( '/^#[0-9A-Fa-f]{6}$/', $raw ) ) { return null; } return $raw; } /** * Optional E.164 phone. * * @param mixed $value Incoming field. */ private function optional_phone( mixed $value ): ?PhoneNumber { $raw = $this->optional_string( $value ); return null === $raw ? null : new PhoneNumber( $raw ); } /** * Optional email. * * @param mixed $value Incoming field. */ private function optional_email( mixed $value ): ?EmailAddress { $raw = $this->optional_string( $value ); return null === $raw ? null : new EmailAddress( $raw ); } }