Struktura modułu notyfikacji w OOP

0

Cześć,
Pomimo tego, że pracuję jako php dev od kilku lat, nigdy nie musiałem nic projektować od zera, zawsze korzystałem z frameworka który swój styl narzuca, a jak czegoś brakowało to wystarczyło dograć paczkę z composer i problem z głowy. Postanowiłem nadrobić swój słaby punkt i mocno podszlifować znajomość OOP i projektowanie aplikacji zgodnie z nim. Zdecydowałem, że napiszę moduł powiadomień sms, email oraz push od zera. Poniżej struktura, którą póki co zaplanowałem korzystając z obecnej wiedzy na temat OOP.

config.ini

server=test
user=root
pass=pass
dbname=mydb

Config class

<?php
   
   /**
    * Class Config
    */
   class Config
   {
       /** @var array|false */
       public $config;
   
       /**
        * Config constructor.
        * @param string $fileName
        * @throws Exception
        */
       public function __construct(string $fileName)
       {
           if (file_exists($fileName)) {
               $this->config = parse_ini_file($fileName, false);
           } else {
               throw new Exception('File ' . $fileName . 'not exists!');
           }
   
       }
   
       /**
        * @param string $key
        * @return mixed
        */
       public function get(string $key)
       {
           return $this->config[$key];
       }
   }

Database class

<?php
  
  /**
   * Class Database
   */
  class Database
  {
      private array $options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,);
      protected $conn;
      private Config $config;
  
      public function __construct(Config $config)
      {
          $this->config = $config;
      }
  
      /**
       * @return PDO
       * @throws Exception
       */
      public function getConnection()
      {
          try {
              $this->conn = new PDO(
                  $this->config['server'],
                  $this->config['user'],
                  $this->config['pass'],
                  $this->options
              );
  
              return $this->conn;
          } catch (PDOException $e) {
              throw new Exception($e->getMessage());
          }
      }
  
      public function closeConnection()
      {
          $this->conn = null;
      }
  }

class Notification

<?php

    /**
     * Class Notification
     */
    class Notification
    {
        const TYPE_SMS = 0;
        const TYPE_EMAIL = 1;
    
        /**
         * @var int
         */
        private int $id;
    
        /**
         * @var string
         */
        private string $phone;
    
        /**
         * @var string
         */
        private string $email;
    
        /**
         * @var string
         */
        private string $message;
    
        /**
         * @var bool
         */
        private bool $isSent;
    
        /**
         * @var int
         */
        private int $type;
    
        /**
         * @return int
         */
        public function getId(): int
        {
            return $this->id;
        }
    
        /**
         * @param int $id
         */
        public function setId(int $id): void
        {
            $this->id = $id;
        }
    
        /**
         * @return string
         */
        public function getPhone(): string
        {
            return $this->phone;
        }
    
        /**
         * @param string $phone
         */
        public function setPhone(string $phone): void
        {
            $this->phone = $phone;
        }
    
        /**
         * @return string
         */
        public function getEmail(): string
        {
            return $this->email;
        }
    
        /**
         * @param string $email
         */
        public function setEmail(string $email): void
        {
            $this->email = $email;
        }
    
        /**
         * @return string
         */
        public function getMessage(): string
        {
            return $this->message;
        }
    
        /**
         * @param string $message
         */
        public function setMessage(string $message): void
        {
            $this->message = $message;
        }
    
        /**
         * @return bool
         */
        public function isSent(): bool
        {
            return $this->isSent;
        }
    
        /**
         * @param bool $isSent
         */
        public function setIsSent(bool $isSent): void
        {
            $this->isSent = $isSent;
        }
    
        /**
         * @return int
         */
        public function getType(): int
        {
            return $this->type;
        }
    
        /**
         * @param int $type
         */
        public function setType(int $type): void
        {
            $this->type = $type;
        }
    }

NotificationDAO

<?php
  
  /**
   * Class NotificationDAO
   */
  class NotificationDAO
  {
      /** @var Database */
      private Database $db;
  
      /**
       * NotificationDAO constructor.
       * @param Database $database
       */
      public function __construct(Database $database)
      {
          $this->db = $database;
      }
  
      /**
       * @param Notification $notification
       */
      public function add(Notification $notification)
      {
          // TODO: Implement add(Notification $notification) method.
      }
  
      /**
       * @param int $notificationId
       */
      public function delete(int $notificationId)
      {
          // TODO: Implement delete(int $notificationId) method.
      }
  
      /**
       * @param Notification $notification
       * @param int $notificationId
       */
      public function update(Notification $notification, int $notificationId)
      {
          // TODO: Implement update(Notification $notification, int $notificationId) method.
      }
  
      /**
       * @param int $notificationId
       */
      public function findOne(int $notificationId)
      {
          // TODO: Implement findOne(int $notificationId) method.
      }
  
  
      public function findAll()
      {
          // TODO: Implement findAll() method.
      }

}

NotificationInterface


    <?php
    
    /**
     * Class NotificationInterface
     */
    interface NotificationInterface
    {
        /**
         * @param Notification $notification
         * @return mixed
         */
        public function sendNotification(Notification $notification);
    }

EmailNotification

<?php
  
  /**
   * Class EmailNotification
   */
  class EmailNotification implements NotificationInterface
  {
      /**
       * @param Notification $notification
       * @return mixed|void
       */
      public function sendNotification(Notification $notification)
      {
          // TODO: Implement sendNotification() method.
      }
  }

SmsNotification


   <?php
    
    /**
     * Class SmsNotification
     */
    class SmsNotification implements NotificationInterface
    {
        /**
         * @param Notification $notification
         * @return mixed|void
         */
        public function sendNotification(Notification $notification)
        {
            // TODO: Implement sendNotification() method.
        }
    }

PushNotification

   <?php
    
    /**
     * Class PushNotification
     */
    class PushNotification implements NotificationInterface
    {
        /**
         * @param Notification $notification
         * @return mixed|void
         */
        public function sendNotification(Notification $notification)
        {
            // TODO: Implement sendNotification() method.
        }
    }

Validation class odpowiedzialna za sprawdzenie email, numer telefonu i wybrany typ

    <?php
    
    /**
     * Class Validation
     */
    class Validation
    {
        /**
         * @param string $value
         */
        public function validateEmail(string $value)
        {
            // TODO: Implement validateEmail(string $value) method.
        }
    
        /**
         * @param string $value
         */
        public function validatePhone(string $value)
        {
            // TODO: Implement validatePhone(string $value) method.
        }
    
        /**
         * @param int $value
         */
        public function validateType(int $value)
        {
            // TODO: Implement validateType(int $value) method.
        }
    }

Czy to jest dobrze zaplanowana struktura, czy powinno to wyglądać inaczej? Co mogę zmienić/poprawić?

3

Odnośnie architektury:

  1. IMO sensowniej byłoby mieć Config::__construct(array $config) oraz Config::from_ini_file(string $fileName): Config - dzięki temu masz nie-failujący konstruktor, który możesz wykorzystać w testach (zamiast go mockować) oraz jak na dłoni widoczne co robi metoda wczytująca konfigurację z pliku.

  2. Config::get() powinno IMO rzucać wyjątek gdy klucz nie istnieje.

  3. W jakim celu istnieje metoda Database::closeConnection()? Skoro nie ma jawnego openConnection(), symetrycznie: nie powinno mnie obchodzić kiedy połączenie się kończy.

  4. NotificationDAO jest zespolone z bazą danych. IMO wygodniejszy byłby interface NotificationRepository + class DatabaseNotificationRepository.

  5. NotificationDAO::update() jest generyczną metodą do robienia wszystkiego, that's a no-go. Metody w repozytorium (którym ta klasa miała być, jak mniemam) powinny realizować konkretny kejs - np.: NotificationDAO::markAsViewed(Notification $notification).

  6. W jakim celu istnieje NotificationDAO::findAll() - czy jakikolwiek biznesowy use case przewiduje pobieranie i wyświetlanie wszystkich powiadomień?

  7. NotificationInterface - skoro nie nazywasz klas z wykorzystaniem sufiksu Class (nie masz przecież ani ConfigClass, ani DatabaseClass), dlaczego wykorzystujesz sufiks Interface do interfejsów? Zauważ, że im głębiej w las, tym mniej kogoś obchodzi czy coś jest interfejsem, czy może konkretną klasą, tak długo, jak udostępnia pożądane metody. Pisząc nowy serwis nie obchodzi Cię przecież czy Database jest class, final class czy też może interface - udostępnia odpowiedni zestaw metod i tyle.

  8. IMO zamiast NotificationInterface::sendNotification() powinno być Notifier::notify() - krótko, zwięźle i bez oszukiwania.

  9. Zamiast god-object Validation IMO lepiej byłoby mieć interfejs Validator oraz osobne klasy w stylu EmailValidator, PhoneValidator etc. Taki układ byłby znacznie prostszy w testowaniu oraz bardziej konfigurowalny (np. w konstruktorze do PhoneValidator mógłbyś określić czy dozwolone są kody krajów).

Inne uwagi:

  1. Komentarze w stylu Class Validation, Config constructor. albo @param string $value (tj. takie, które dokumentują rzeczy już znane) są IMO całkowicie zbędnym szumem. Komentarze powinny wnosić do kodu coś nowego, a nie tłumaczyć to, co mam napisane linijkę niżej (przecież widzę, że to klasa Validation; przecież widzę, że to konstruktor klasy Config; przecież widzę, że ten parametr jest typu string).

  2. W jakim celu masz throw new Exception($e->getMessage());? Stracisz przez to oryginalny stacktrace wyjątku, nie zyskując nic w zamian.

  3. Notification - to wręcz niesamowite, jak udało Ci się nadmuchać kod, który nie robi nic więcej niż:

    <?php
    
    class Notification {
      public int $id;
      public string $phone;
      public string $email;
      public string $message;
      public bool $isSent;
      public NotificationType $type;
    }
    
    class NotificationType {
      const SMS = 'sms';
      const EMAIL = 'email';
    
      public static sms(): Self {
        /* ... */
      }
    
      public static email(): Self {
        /* ... */
      }
    }
    
    // w gruncie rzeczy mój kod nie tylko jest krótszy, ale również nie pozwala na wprowadzenie głupot do pola `type`; win-win
    

    ... innymi słowy: nie mnóż bytów ponad potrzebę; każda abstrakcja powinna wnosić coś nowego, a nie po prostu biernie istnieć.

0

@Patryk27: Witaj i dziękuję za odpowiedź, odnośnie nich:

  1. Racja
  2. Mój błąd, a jest to ważna rzecz którą pominąłem
  3. Jakby się tak zastanowić to faktycznie niepotrzebna
  4. No właśnie wszystkie akcji bardziej pasowały mi pod repozytorium, a zrobiłem zupełnie inaczej
  5. Zrozumiałem :D
  6. Do utworzenia listy wszystkich powiadomień z paginacją
  7. No właśnie z tymi nazwali, trochę głupio mi się to wydawało, bo co kogo obchodzi czy dany plik jest interfejsem, czy czymś innym
  8. Mam dziwny nawyk dawania takich nietypowych nazw. Już sama nazwa klasy/interfejsu mówi za co on odpowiada, więc w metodzie już powinienem to pominąć.
  9. Podobną rzecz chciałem zrobić, ale wydawało mi się, że zbyt przesadnie chcę rozbić walidację na kilka klas.

Co do innych uwag:

  1. Szczerze mówiąc większość tych komentarzy tworzy mi PHP Storm, ale zapamiętam
  2. Tu jest taki problem, że nie bardzo wiem, jak we własnej klasie raportować błędy, nie zrobię echo w klasie bo to słabo wygląda.
  3. Tutaj trochę popłynąłem Entity w Symfony, stąd te get/set

Jak sam zauważyłeś, mam spore braki w projektowaniu OOP i dzieleniu na klasy, to raczej z racji tego, że zamiast najpierw porządnie ogarnąć OOP usiadłem do frameworka który jakiś styl wymuszał.

1

Do utworzenia listy wszystkich powiadomień z paginacją

Do utworzenia paginacji potrzebujesz pobrać wszystkie powiadomienia?

Tu jest taki problem, że nie bardzo wiem, jak we własnej klasie raportować błędy, nie zrobię echo w klasie bo to słabo wygląda.

Po co w ogóle łapiesz ten wyjątek? Wystarczyłoby, abyś nie wrzucił tam żadnego try, żadnego catch i niech ktoś wyżej się tym martwi; nie musisz łapać każdego wyjątku :-)

0

@Patryk27:

  1. Przy findAll trochę popłynąłem, bo metoda powinna przyjmować parametry do paginacji :)
  2. Kiedyś to olewałem to mi ludzie zarzucali, że nie przechwytuje wyjątków ani nie wyświetlam błędów w klasie. Robiłem tak, że pomijałem wyjątki i łapałem je dopiero w momencie gdzie używałem danej klasy.
1

dlaczego config to plik ini a nie normalny plik php kóry wystarczy includować zamiast parsować?

0
Miang napisał(a):

dlaczego config to plik ini a nie normalny plik php kóry wystarczy includować zamiast parsować?

Może dlatego żeby syntax error, compile error albo runtime error w configu nie położył aplikacji.

Ewentualnie żeby mieć rożne configi pod różne środowiska, co byłoby dziwne z include'owanym configiem w PHP.

2
 /**
     * Class Database
     */
    class Database

cat.png

1 użytkowników online, w tym zalogowanych: 0, gości: 1