2

I'm separating my software into three main layers (maybe tiers would be a better term):

  • Presentation ('Views')
  • Business logic ('Services' and 'Repositories')
  • Data access ('Entities' (e.g. ActiveRecords))

What do I have now?

In Presentation, I use read-only access to Entities, returned from Repositories or Services, to display data.

$banks = $banksRegistryService->getBanksRepository()->getBanksByCity( $city );

$banksViewModel = new PaginatedList( $banks ); // some way to display banks;
                                               // example, not real code

I find this approach quite efficient in terms of performance and code maintanability and still safe as long as all write operations (create, update, delete) are preformed through a Service:

namespace Service\BankRegistry;

use Service\AbstractDatabaseService;
use Service\IBankRegistryService;
use Model\BankRegistry\Bank;

class Service
    extends AbstractDatabaseService
    implements IBankRegistryService
{
    /**
     * Registers a new Bank
     *
     * @param string $name                  Bank's name
     * @param string $bik                   Bank's Identification Code
     * @param string $correspondent_account Bank's correspondent account
     *
     * @return Bank
     */
    public function registerBank( $name, $bik, $correspondent_account )
    {
        $bank = new Bank();

        $bank
            -> setName( $name )
            -> setBik( $bik )
            -> setCorrespondentAccount( $correspondent_account );


        if( null === $this->getBanksRepository()->getDefaultBank() )
            $this->setDefaultBank( $bank );


        $this->getEntityManager()->persist( $bank );

        return $bank;
    }


    /**
     * Makes the $bank system's default bank
     *
     * @param Bank $bank
     * @return IBankRegistryService
     */
    public function setDefaultBank( Bank $bank )
    {
        $default_bank = $this->getBanksRepository()->getDefaultBank();  

        if( null !== $default_bank )
            $default_bank->setDefault( false );

        $bank->setDefault( true );

        return $this;
    }
}

Where am I stuck?

I'm struggling about how to update certain fields in Bank Entity.

Bad solution #1: Making a series of setters in Service for each setter in Bank; - seems to be quite reduntant, increases Service interface complexity and proportionally decreases it's simplicity - something to avoid if you care about code maitainability. I try to follow KISS and DRY principles.

Bad solution #2: Modifying Bank directly through it's native setters; - really bad. If you'll ever need to move modification into the Service, it will be pain. Business logic should remain in Business logic layer. Plus, there are plans on logging all of the actions and maybe even involve user permissions (perhaps, through decorators) in future, so all modifications should be made only through the Service.

Possible good solution: Creating an updateBank( Bank $bank, $array_of_fields_to_update) method; - makes the interface as simple as possible, but there is a problem: one should not try to manually set isDefault flag on a Bank, this operation should be performed through setDefaultBank method. It gets even worse when you have relations that you don't want to be directly modified.

Of course, you can just limit the fields that can be modified by this method, but how do you tell method's user what they can and cannot modify? Exceptions?

  • What about a single updateBank() setter in Service, to which you pass an object which is a composite of all the Bank properties that you are allowed to update this way, and no other properties? – Julia Hayward Jan 6 '15 at 14:25
1

The decision I've come up with

I've made setDefaultBank a protected method and removed it from IBankService public interface. I've also created an updateBank( $bank, array $fields ) method that filters the $fields array and calls corresponding setters on Bank. If a default field is encountered, it is checked to be equal to true and setDefaultBank is called. Any invalid elements in $fields cause an \InvalidArgumentException to be thrown.

/**
 * Updates Bank fields
 *
 * @param int|Bank $bank    Bank ID or instance
 * @param array    $fields  Associative array of Bank fields
 *
 * @return IBankRegistryService
 * @throws \InvalidArgumentException
 */
public function updateBank( $bank, array $fields )
{
    if(    false === $bank instanceof Bank
        && 0 < intval( $bank ) )
    {
        /** @var $bank Bank */
        $bank = $this->getBanksRepository()->find( $bank );
    }
    else
        throw new \InvalidArgumentException( '$bank should be either an ID or a Bank instance.' );

    foreach( $fields AS $key => $value )
    {
        switch( $key )
        {
            case 'bik':
                $bank->setBik( $value );
                break;

            case 'name':
                $bank->setName( $value );
                break;

            case 'correspondent_account':
                $bank->setCorrespondentAccount( $value );
                break;

            case 'default':
                if( !$value )
                    throw new \InvalidArgumentException( 'Property "default" can only be equal to true' );

                $this->setDefaultBank( $bank );

                break;

            default:
                throw new \InvalidArgumentException( 'Cannot change "' . $key . '" property of a Bank' );
        }
    }

    return $this;
}
-1

Maybe CQRS is worth looking at for you.

http://martinfowler.com/bliki/CQRS.html

More specifically:

Interacting with the command-model naturally falls into commands or events, which meshes well with Event Sourcing.

So in this pattern, you send updates to the domain model using commands sent via. events.

  • how does this answer the question asked, "how to update certain fields in Bank Entity.... how do you tell method's user what they can and cannot modify?" – gnat Oct 8 '14 at 9:20
  • He came up with 2 bad solutions and 1 possible good solutions. I referenced another possible good solution which is in use by others, and which he can search for. As I understood the question, he was looking for ways to update a Bank and event sourcing is one such way. – Kresten Kjær Oct 8 '14 at 18:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.