phpormpersistent-object-store

Infinite loop with constructors due to m:n relation


I have a database with authors and books, m:n authors (a_id, ...) authors_books (a_id, b_id) books (b_id, ...)

My problem is, that I can't use the constructors to fetch the author/book-data into an array, because I would get an infinite loop.

class Book
{

    public $name;
    public $authors;

    public function __construct($name)
    {
        $this->name=$name;
        $this->authors=$this->Get_Authors();
    }

    public function Get_Authors()
    {
        $authors=array();
        /* ... (database) */
        $authors[]=new Author($name_from_db);
        return $authors;
    }

}

class Author
{

    public $name;
    public $books;

    public function __construct($name)
    {
        $this->name=$name;
        $this->books=$this->Get_Books();
    }

    public function Get_Books()
    {
        $books=array();
        /* ... (database) */
        $books[]=new Book($name_from_db);
        return $books;
    }

}

Example:

new Book('book_1');

-> is going to fetch 'author_1' and uses __constructor of Author class

new Author('author_1');

-> is going to fetch 'book_1 and uses __constructor of Book class ...

What is the "best practice" to resolve a m:n relation in PHP classes?


Solution

  • You can use lazy loading here:

    class Book {
    
        public $name;
        private $_authors = null;
    
        public function __constructor($name) {
            $this->name = $name;
        }
    
        public function getAuthors() {
            if ($this->_authors === null) {
                $this->_authors = array();
                /* database */
                $this->_authors[] = new Author(/**/);
            }
            return $this->_authors;
        }
    
        // You can add some magic getter if you want to access authors as property
        public function __get($key) {
            if ($key === 'authors') {
                return $this->getAuthors();
            }
            throw new Exception('Unknown property '.$key);
        }
    }
    
    class Authors {
    
        public $name;
        private $_books = null;
    
        public function __constructor($name) {
            $this->name = $name;
        }
    
        public function getBooks() {
            if ($this->_books === null) {
                $this->_books = array();
                /* database */
                $this->_books[] = new Book(/**/);
            }
            return $this->_books;
        }
    
        // You can add some magic getter if you want to access books as property
        public function __get($key) {
            if ($key === 'books') {
                return $this->getBooks();
            }
            throw new Exception('Unknown property '.$key);
        }
    }
    

    This will cause that authors/books will be loaded only if you'll need it and won't loop infinitely, but you can reach another problem here:

    $author = new Author("Jon Doe");
    $book = $author->books[0];
    // assuming that book has one author
    // $book->authors[0] will not be same object as $author
    

    Solution for that would be to use some third object for loading books and authors, that will store already loaded objects and inject them in proper places

    class Library {
    
        private $_books = array();
        private $_authors = array();
    
        public function getBooksForAuthor($authorId) {
            /* db query... */
            $books = array();
            while ($row = $stmt->fetch()) {
                if (isset($this->_books[$row['id']]) {
                    $books[] = $this->_books[$row['id']];
                } else {
                    $book = new Book($row);
                    $this->_books[$row['id']] = $book;
                    $books[] = $book;
                }
            }
            return $books;
        }
    
        /* and similar authorsForBook() method */
    }
    
    class Author {
    
        private $_data;
        private $_library;
        private $_books = null;
    
        public function __constructor($data, $library) {
            $this->_data = $data;
            $this->_library = $library;
        }
    
        public function getBooks() {
            if ($this->_books === null) {
                $this->_books = $this->_library->getBooksForAuthor($this->_data['id']);
            }
            return $this->_books;
        }
    
        public function __get($key) {
            if ($key === 'books') {
                return $this->getBooks();
            }
            if (isset($this->_data[$key]) {
                return $this->_data[$key];
            }
            throw new Exception('Unknown property '.$key);
        }
    }
    
    /* and similar for book */