8부 – WordPress 및 객체 지향 프로그래밍: WordPress 예제 – 구현: 옵션

게시 됨: 2022-02-04

지금까지는 사용자 정의 옵션만 저장하면 되므로 설정 API를 활용했습니다. 그러나 우리 플러그인은 IP 주소가 로그인에 실패한 횟수, 현재 잠겨 있는 경우 등을 "기억"하기 위해 옵션 자체를 읽고 쓸 수 있어야 합니다.

옵션을 저장하고 검색하는 객체 지향 방식이 필요합니다. "디자인" 단계에서 우리는 이에 대해 간략하게 논의했지만 옵션 가져오기 , 설정제거 와 같이 우리가 수행할 수 있기를 원하는 작업에만 초점을 맞춰 구현 세부 사항 중 일부를 추상화했습니다.

또한 섹션을 기반으로 "그룹화" 옵션을 함께 분류하여 정리할 것입니다. 순전히 개인의 취향에 따른 것입니다.

이것을 인터페이스로 바꾸자:

 interface Options { /** * Return the option value based on the given option name. * * @param string $name Option name. * @return mixed */ public function get( $name ); /** * Store the given value to an option with the given name. * * @param string $name Option name. * @param mixed $value Option value. * @param string $section_id Section ID. * @return bool Whether the option was added. */ public function set( $name, $value, $section_id ); /** * Remove the option with the given name. * * @param string $name Option name. * @param string $section_id Section ID. */ public function remove( $name, $section_id ); }

이상적으로는 다음과 같이 하여 WordPress Options API와 상호 작용할 수 있습니다.

 $options = new WP_Options(); $options->get( 'retries' );

이 시점에서 우리는 왜 우리 고유의 인터페이스와 클래스를 만드는 문제에 빠지는 대신 get_option() WordPress 함수를 사용하지 않는지 궁금할 것입니다. WordPress 기능을 직접 사용하는 것은 플러그인을 개발하는 데 완벽하게 허용되는 방법이지만 한 단계 더 나아가 의존할 인터페이스를 만들어 유연성을 유지합니다.

WP_Options 클래스는 Options 인터페이스를 구현할 것입니다. 그렇게 하면 향후 요구 사항이 변경될 경우에 대비할 수 있습니다. 예를 들어, 사용자 지정 테이블, 외부 데이터베이스, 메모리(예: Redis)에 옵션을 저장해야 할 수도 있습니다. 추상화(즉, 인터페이스)에 따라 구현에서 무언가를 변경하는 것은 동일한 인터페이스를 구현하는 새 클래스를 만드는 것만큼 간단합니다.

WP_옵션

생성자에서 get_option() WordPress 함수를 사용하여 모든 옵션을 검색하여 WP_Options 클래스 작성을 시작하겠습니다.

 class WP_Options { /** * @var array Stored options. */ private $options; /** * WP_Options constructor. */ public function __construct() { $this->options = get_option( Plugin::PREFIX ); } }

$options 속성은 내부적으로 사용되므로 private 로 선언하여 이를 정의한 클래스인 WP_Options 클래스에서만 액세스할 수 있도록 합니다.

이제 implements 연산자를 사용하여 Options 인터페이스를 구현해 보겠습니다.

 class WP_Options implements Options { // ...

IDE는 클래스 추상을 선언하거나 인터페이스에 정의된 get() , set()remove() 메서드를 구현하도록 우리에게 소리칩니다.

자, 이제 이 방법들을 구현해 봅시다!

옵션 얻기

$options 속성에서 지정된 옵션 이름을 찾고 해당 값을 반환하거나 존재하지 않는 경우 false 를 반환하는 get() 메서드로 시작하겠습니다.

 class WP_Options implements Options { private $options; public function __construct() { $this->options = get_option( Plugin::PREFIX ); } /** * Return the option value based on the given option name. * * @return mixed */ public function get( $option_name ) { if ( ! isset( $this->options[ $option_name ] ) ) { return false; } return $this->options[ $option_name ]; } }

이제 기본 옵션에 대해 생각할 때입니다.

기본 옵션

이전에 언급했듯이 섹션을 기반으로 옵션을 함께 그룹화하고 싶습니다. 따라서 옵션을 몇 개의 섹션으로 나눌 것입니다. "일반 옵션" 섹션과 우리가 추적해야 하는 데이터에 대한 또 다른 섹션. 잠금, 재시도, 잠금 로그 및 총 잠금 횟수 - 이 상태를 임의로 호출합니다.

상수 를 사용하여 기본 옵션을 저장합니다. 상수 값은 코드가 실행되는 동안 변경할 수 없으므로 기본 옵션과 같은 것에 이상적입니다. 클래스 상수는 각 클래스 인스턴스가 아니라 클래스당 한 번 할당됩니다.

참고: 상수 이름은 규칙에 따라 모두 대문자입니다.

 const DEFAULT_OPTIONS = array( 'general_options' => array( 'allowed_retries' => 4, 'normal_lockout_time' => 1200, // 20 minutes 'max_lockouts' => 4, 'long_lockout_time' => 86400, // 24 hours 'hours_until_retries_reset' => 43200, // 12 hours 'site_connection' => 'direct', 'handle_cookie_login' => 'yes', 'notify_on_lockout_log_ip' => true, 'notify_on_lockout_email_to_admin' => false, 'notify_after_lockouts' => 4 ), 'state' => array( 'lockouts' => array(), 'retries' => array(), 'lockout_logs' => array(), 'total_lockouts' => 0 ) );

DEFAULT_OPTIONS 중첩 배열에서 모든 옵션에 대한 기본값을 설정했습니다.

다음으로 하고 싶은 것은 플러그인이 초기화되면 add_option() WordPress 함수를 사용하여 데이터베이스에 기본 옵션 값을 저장하는 것입니다.

 class WP_Options { public function __construct() { $all_options = array(); foreach ( self::DEFAULT_OPTIONS as $section_id => $section_default_options ) { $db_option_name = Plugin::PREFIX . '_' . $section_id; $section_options = get_option( $db_option_name ); if ( $section_options === false ) { add_option( $db_option_name, $section_default_options ); $section_options = $section_default_options; } $all_options = array_merge( $all_options, $section_options ); } $this->options = $all_options; } }

이 스니펫을 자세히 살펴보겠습니다. 먼저 기본 옵션 배열을 반복하고 get_option() WordPress 함수를 사용하여 옵션을 검색합니다.

 foreach ( self::default_options as $section_id => $section_default_options ) { $db_option_name = Plugin::PREFIX . '_' . $section_id; $section_options = get_option( $db_option_name ); // ...

그런 다음 각 옵션이 데이터베이스에 이미 존재하는지 확인하고, 없으면 기본 옵션을 저장합니다.

 if ( $section_options === false ) { add_option( $db_option_name, $section_default_options ); $section_options = $section_default_options; }

마지막으로 모든 섹션의 옵션을 수집합니다.

 $all_options = array_merge( $all_options, $section_options );

그리고 나중에 액세스할 수 있도록 $options 속성에 저장합니다.

 $this->options = $all_options;

데이터베이스의 WordPress 옵션 테이블에는 두 개의 행이 있습니다. 여기서 option_name 은 섹션 이름에 연결된 플러그인의 접두사로 구성됩니다.

이제 구현해야 하는 나머지 메서드로 이동하겠습니다.

옵션 저장

마찬가지로, 데이터베이스에 새 옵션을 쉽게 저장하고 다음과 같이 이전 값을 덮어쓰고 싶습니다.

 $options = new Options(); $options->set( 'retries', 4 );

이제 update_option() WordPress 함수를 사용할 set() 메서드를 구현해 보겠습니다.

 /** * Store the given value to an option with the given name. * * @param string $name Option name. * @param mixed $value Option value. * @param string $section_id Section id. Defaults to 'state'. * @return bool Whether the option was added. */ public function set( $name, $value, $section_ ) { $db_option_name = Plugin::PREFIX . '_' . $section_id; $stored_option = get_option( $db_option_name ); $stored_option[ $name ] = $value; return update_option( $db_option_name, $stored_option ); }

옵션 제거

마지막으로 옵션을 초기 값으로 설정하는 remove() 메서드를 구현합니다.

 /** * Remove the option with the given name. * * @param string $name Option name. * @param string $section_id Section id. Defaults to 'state'. * @return bool Whether the option was removed. */ public function remove( $name, $section_ ) { $initial_value = array(); if ( isset( self::DEFAULT_OPTIONS[ $section_id ][ $name ] ) ) { $initial_value = self::DEFAULT_OPTIONS[ $section_id ][ $name ]; } return $this->set( $name, $initial_value, $section_id ); }

우리는 모든 것을 하나의 클래스로 묶었습니다. 모든 옵션 관련 데이터(예: 속성) 및 구현 세부 정보(예: 방금 구현한 메서드)는 WP_Options 클래스에 캡슐화 됩니다.

캡슐화/추상화

모든 것을 단일 클래스로 래핑하고 내부를 (마치 캡슐에 넣은 것처럼) 외부 세계로부터 본질적으로 "숨기는" 것을 캡슐화 라고 합니다. 캡슐화는 객체 지향 프로그래밍의 또 다른 핵심 개념입니다.

Pressidium으로 웹사이트 호스팅

60일 환불 보장

계획 보기

Options 인터페이스를 사용하여 옵션의 개념을 추상화하고 개념적으로 단순화하여 옵션을 수행 하는 방법 대신 옵션으로 수행 하는 작업에 집중했습니다. 이것은 객체 지향 프로그래밍의 또 다른 핵심 개념인 추상화 라고 합니다.

캡슐화와 추상화는 완전히 다른 개념이지만 분명히 알 수 있듯이 매우 관련이 있습니다. 주요 차이점은 캡슐화는 구현 수준에 있고 추상화는 디자인 수준에 있다는 것입니다.

종속성

다음 시나리오를 고려해 보겠습니다.

IP 주소가 잠겨야 하는지 여부, 해당 잠금 기간이 어떻게 되어야 하는지, 활성 잠금이 여전히 유효하거나 만료된 경우 등을 결정하는 should_get_locked_out() 을 하는 Lockouts 클래스가 있습니다. 이 클래스에는 IP 주소를 잠가야 하는지 여부. 이 방법은 IP 주소가 잠기기 전에 허용되는 최대 재시도 횟수를 읽어야 하며, 이는 구성 가능한 값이며 이는 옵션 으로 저장됨을 의미합니다.

따라서 방금 설명한 코드는 다음과 유사합니다.

 class Lockouts { // ... /** * @var WP_Options An instance of `WP_Options`. */ private $options; /** * Lockouts constructor */ public function __construct() { $this->options = new WP_Options(); } /** * Return the number of retries. * * @return int */ private function get_number_of_retries() { // ... } /** * Check whether this IP address should get locked out. * * @return bool */ public function should_get_locked_out() { $retries = $this->get_number_of_retries(); $allowed_retries = $this->options->get( 'allowed_retries' ); return $retries % $allowed_retries === 0; } // ... }

기본적으로 생성자에서 WP_Options 의 새 인스턴스를 만든 다음 해당 인스턴스를 사용하여 allowed_retries 옵션의 값을 검색합니다.

그것은 절대적으로 괜찮지 만 Lockouts 클래스가 이제 WP_Options 에 의존한다는 것을 명심해야 합니다. 우리는 WP_Options를 의존성 이라고 부릅니다.

예를 들어 미래에 요구 사항이 변경되면 외부 데이터베이스에서 옵션을 읽고 써야 하는 경우 WP_OptionsDB_Options 클래스로 교체해야 합니다. 한 클래스에서만 옵션을 검색해야 하는 경우에는 그렇게 나쁘지 않은 것 같습니다. 그러나 여러 종속성이 있는 클래스가 많을 때는 약간 까다로울 수 있습니다. 단일 종속성에 대한 변경 사항은 코드베이스 전체에 영향을 미치므로 종속성 중 하나가 변경되면 클래스를 수정해야 합니다.

종속성 반전 원칙 을 따르도록 코드를 다시 작성하여 이 문제를 제거할 수 있습니다.

디커플링

SOLID의 "D"인 DIP(Dependency Inversion Principle)는 다음과 같이 설명합니다.

  • 고수준 모듈은 저수준 모듈에서 아무것도 가져오지 않아야 합니다. 둘 다 추상화에 의존해야 합니다.
  • 추상화는 세부 사항에 의존해서는 안됩니다. 세부사항(구체적인 구현)은 추상화에 의존해야 합니다.

우리의 경우 Lockouts 클래스는 "고수준 모듈"이며 "저수준 모듈"인 WP_Options 클래스에 종속됩니다.

생각보다 쉬운 Dependency Injection 을 사용하여 변경할 것입니다. Lockouts 클래스는 객체를 생성하는 대신 의존하는 객체를 받습니다.

 class Lockouts { // ... /** * Lockouts constructor. * * @param WP_Options $options */ public function __construct( WP_Options $options ) { $this->options = $options; } // ... }

따라서 종속성을 주입 합니다.

 $options = new WP_Options(); $lockouts = new Lockouts( $options );

이제 Lockouts 클래스가 WP_Options 종속성과 느슨하게 결합 되었기 때문에 유지 관리가 더 쉬워졌습니다. 또한 종속성을 조롱하여 코드를 더 쉽게 테스트할 수 있습니다. WP_Options 를 동작을 모방하는 객체로 교체하면 실제로 데이터베이스에서 쿼리를 실행하지 않고도 코드를 테스트할 수 있습니다.

 /** * Lockouts constructor. * * @param WP_Options $options */ public function __construct( WP_Options $options ) { $this->options = $options; }

Lockouts 의 종속성에 대한 제어를 다른 클래스에 부여했지만( WP_Options Lockouts Lockouts 예상합니다. 즉, 추상화 대신 구체적인 WP_Options 클래스에 여전히 의존합니다. 앞서 언급했듯이 두 모듈 모두 추상화에 의존해야 합니다 .

수정하자!

 /** * Lockouts constructor. * * @param Options $options */ public function __construct( Options $options ) { $this->options = $options; }

그리고 단순히 $options 인수의 유형을 WP_Options 클래스에서 Options 인터페이스로 변경함으로써 Lockouts 클래스는 추상화에 의존하고 DB_Options 객체 또는 동일한 인터페이스를 구현하는 모든 클래스의 인스턴스를 자유롭게 전달할 수 있습니다. 생성자에게.

단일 책임

IP 주소가 잠겨야 하는지 여부를 확인하기 위해 should_get_locked_out() 이라는 메서드를 사용했다는 점은 주목할 가치가 있습니다.

 /** * Check whether this IP address should get locked out. * * @return bool */ public function should_get_locked_out() { $retries = $this->get_number_of_retries(); $allowed_retries = $this->options->get( 'allowed_retries' ); return $retries % $allowed_retries === 0; }

다음과 같이 한 줄로 쉽게 작성할 수 있습니다.

 if ( $this->get_number_of_retries() % $this->options->get( 'allowed_retries' ) === 0 ) {

그러나 해당 논리 조각을 고유한 방법으로 옮기면 많은 이점이 있습니다.

  • IP 주소를 잠가야 하는지 여부를 결정하는 조건이 변경되면 이 메서드만 수정하면 됩니다(if 문의 모든 항목을 검색하는 대신).
  • 각 "단위"가 더 작을 때 단위 테스트 작성이 더 쉬워집니다.
  • 코드의 가독성을 많이 향상시킵니다.

이것을 읽고:

 if ( $this->should_get_locked_out() ) { // ...

다음을 읽는 것보다 훨씬 쉬운 것 같습니다.

 if ( $this->get_number_of_retries() % $this->options->get( 'allowed_retries' ) === 0 ) { // ...

플러그인의 거의 모든 방법에 대해 이 작업을 수행했습니다. 더 이상 추출할 것이 없을 때까지 더 긴 것에서 메소드를 추출합니다. 클래스도 마찬가지입니다. 각 클래스와 메서드는 단일 책임을 져야 합니다.

SOLID의 "S"인 단일 책임 원칙(SRP) 은 다음과 같이 명시합니다.

"컴퓨터 프로그램의 모든 모듈, 클래스 또는 기능은 해당 프로그램 기능의 단일 부분에 대한 책임을 져야 하며 해당 부분을 캡슐화해야 합니다."

또는 Robert C. Martin("밥 삼촌")이 말했듯이:

"클래스에는 변경해야 할 단 하나의 이유가 있어야 합니다."

메인 플러그인 파일 재방문

현재 기본 플러그인 파일에는 다음 내용만 포함되어 있습니다.

 /** * Plugin Name: PRSDM Limit Login Attempts * Plugin URI: https://pressidium.com * Description: Limit rate of login attempts, including by way of cookies, for each IP. * Author: Pressidium * Author URI: https://pressidium.com * Text Domain: prsdm-limit-login-attempts * License: GPL-2.0+ * Version: 1.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; }

이번에도 이름 충돌을 피하기 위해 모든 것을 Plugin 클래스로 래핑할 것입니다.

 namespace Pressidium\Limit_Login_Attempts; if ( ! defined( 'ABSPATH' ) ) { exit; } class Plugin { /** * Plugin constructor. */ public function __construct() { // ... } }

생성자에서 코드를 실행할 파일 끝에서 이 Plugin 클래스를 인스턴스화합니다.

 new Plugin();

생성자에서 활성화된 플러그인이 로드되면 실행되는 plugins_loaded 작업에 연결합니다.

 public function __construct() { add_action( 'plugins_loaded', array( $this, 'init' ) ); } public function init() { // Initialization }

또한 모든 PHP 파일을 로드하기 위해 require_files() 메서드를 호출합니다.

 public function __construct() { $this->require_files(); add_action( 'plugins_loaded', array( $this, 'init' ) ); } private function require_files() { require_once __DIR__ . '/includes/Sections/Section.php'; require_once __DIR__ . '/includes/Pages/Admin_Page.php'; require_once __DIR__ . '/includes/Pages/Settings_Page.php'; // ... }

마지막으로 init() 메소드에서 일부 객체를 생성하여 플러그인을 초기화합니다.

참고: 다음 스니펫에는 기본 플러그인 파일의 일부만 포함되어 있습니다. 플러그인의 GitHub 저장소에서 실제 파일을 읽을 수 있습니다.

 public function init() { $options = new Options(); $hooks_manager = new Hooks_Manager(); $settings_page = new Settings_Page( $options ); $hooks_manager->register( $settings_page ); // ... }

파일 정리

파일을 정리하는 것은 특히 코드가 많은 대규모 플러그인을 작업할 때 매우 중요합니다. 폴더 구조는 유사한 파일을 함께 그룹화하여 귀하와 귀하의 팀원이 정리를 유지하는 데 도움이 되어야 합니다.

Pages , Sections , Fields , Elements 등을 위한 여러 하위 네임스페이스를 포함하는 네임스페이스( Pressidium\Limit_Login_Attempts )를 이미 정의했습니다. 디렉토리와 파일을 구성하기 위한 계층 구조를 따라 다음과 유사한 구조가 되었습니다.

 . ├── includes │ ├── Hooks │ │ ├── Actions.php │ │ ├── Filters.php │ │ └── Hooks_Manager.php │ ├── Pages │ │ ├── Admin_Page.php │ │ └── Settings_Page.php │ ├── Sections │ │ ├── Fields │ │ │ ├── Elements │ │ │ │ ├── Checkbox_Element.php │ │ │ │ ├── Custom_Element.php │ │ │ │ ├── Element.php │ │ │ │ ├── Number_Element.php │ │ │ │ └── Radio_Element.php │ │ │ └── Field.php │ │ └── Section.php │ └── WP_Options.php ├── prsdm-limit-login-attempts.php └── uninstall.php

각 파일에는 단일 클래스가 있습니다. 파일은 포함된 클래스의 이름을 따서 명명되고 디렉토리와 하위 디렉토리는 (하위) 네임스페이스의 이름을 따서 명명됩니다.

사용할 수 있는 여러 아키텍처 패턴과 명명 체계가 있습니다. 귀하에게 의미가 있고 프로젝트의 요구 사항에 맞는 것을 선택하는 것은 귀하에게 달려 있습니다. 프로젝트를 구성할 때 중요한 것은 일관성 입니다.

결론

축하합니다! WordPress 및 객체 지향 프로그래밍에 대한 기사 시리즈를 완료했습니다.

몇 가지를 배웠고 배운 내용을 자신의 프로젝트에 적용할 수 있기를 바랍니다!

이 시리즈에서 다룬 내용을 간단히 요약하면 다음과 같습니다.

  • 요구 사항 수집: 플러그인이 무엇을 해야 하는지 결정했습니다.
  • 디자인: 플러그인이 어떻게 구성될지, 잠재적 클래스 간의 관계, 추상화에 대한 높은 수준의 개요에 대해 생각했습니다.
  • 구현: 플러그인의 일부 핵심 부분에 대한 실제 코드를 작성했습니다. 그렇게 하는 동안 몇 가지 개념과 원칙을 소개했습니다.

그러나 우리는 OOP가 무엇인지 그리고 제공해야 하는 것의 표면을 간신히 긁었습니다. 새로운 기술을 익히는 데는 연습이 필요하므로 계속해서 자신만의 객체 지향 WordPress 플러그인 구축을 시작하세요. 즐거운 코딩!

또한보십시오

  • WordPress 및 객체 지향 프로그래밍 – 개요
  • 2부 – WordPress와 객체 지향 프로그래밍: 실제 사례
  • 3부 – WordPress 및 객체 지향 프로그래밍: Α WordPress 예제 – 범위 정의
  • 4부 – WordPress 및 객체 지향 프로그래밍: WordPress 예제 – 디자인
  • 5부 – WordPress 및 객체 지향 프로그래밍: WordPress 예제 – 구현: 관리 메뉴
  • 6부 – WordPress 및 객체 지향 프로그래밍: WordPress 예제 – 구현: 섹션 등록
  • 7부 – WordPress 및 객체 지향 프로그래밍: WordPress 예제 – 구현: WordPress 후크 관리