In my Symfony/Doctrine application, I would like any timestamp fields in my database to retreived as PEAR Date objects instead of date strings. e.g. If my schema is
SomeEvent:
columns:
id:
type: integer
primary: true
start: timestamp
end: timestamp
I would like to be able to run a Doctrine query to retrieve SomeEvent objects and have $anEvent->getStart() be a PEAR Date object. Right now Doctrine gives me strings for all timestamp fields which is basically useless. (I'd also like saving of Dates to work correctly.)
What's the best way to accomplish this?
I researched using a Hydration listener but it looks like I'd have to register that per table and hardcode the column names that I want to be converted. Using a custom Hydrator didn't look much better since then I lose the ability to use any of the other core hydration methods without having my dates be strings again.
EDIT: It looks like Doctrine 2 has a feature that's exactly what I'm looking for: Custom Mapping Types. Unfortunately this site is being deployed to a host that doesn't support PHP 5.3+ so Doctrine 2 is out. :(
I figured out a hacky way to do it that I'm not super happy with, but it works. I created the following 2 classes:
class DateClassBehavior extends Doctrine_Template {
public function setTableDefinition() {
$listener = new DateClassListener();
$table = $this->getTable();
foreach ($table->getColumns() as $columnName => $columnDef) {
if ($columnDef['type'] == 'timestamp') {
$listener->dateColumns[] = $columnName;
}
}
$this->addListener($listener);
}
}
class DateClassListener extends Doctrine_Record_Listener {
public $dateColumns = array();
public function postHydrate(Doctrine_Event $event) {
$data = $event->data;
foreach ($this->dateColumns as $col) {
$date = $data->$col == null ? null : new Date($data->$col);
$data->$col = null;
$data->$col = $date;
}
$event->data = $data;
}
}
And added DateClassBehavior to the definition for each table in my model:
SomeEvent:
actAs: [DateClassBehavior]
columns:
id:
type: integer
primary: true
start: timestamp
end: timestamp
This takes care of creating the Date objects when things are loaded. I also modified the actual PEAR Date class (I know... bad) and added a __toString() method that returns $this->getDate(). PHP then automatically converts the dates to the correct string when doctrine saves them.
If anyone finds a better way to do this please post it.
Related
Long story short.
I use Doctrine's Single Table Inheritance mapping to map three different contexts (classes) of the one common entity: NotActivatedCustomer, DeletedCustomer, and Customer. Also, there is an AbstractCustomer which contains the next:
App\Identity\Domain\Customer\AbstractCustomer:
type: entity
inheritanceType: SINGLE_TABLE
discriminatorColumn:
name: discr
type: string
discriminatorMap:
Customer: App\Identity\Domain\Customer\Customer
NotActivatedCustomer: App\Identity\Domain\Customer\NotActivatedCustomer
DeletedCustomer: App\Identity\Domain\Customer\DeletedCustomer
table: customer
id:
id:
type: customer_id
unique: true
generator:
strategy: CUSTOM
customIdGenerator:
class: Symfony\Bridge\Doctrine\IdGenerator\UuidV4Generator
fields:
email:
type: email
length: 180
unique: true
A Subtype definition example:
<?php
declare(strict_types=1);
namespace App\Identity\Domain\Customer;
use App\Identity\Domain\User\Email;
class DeletedCustomer extends AbstractCustomer
{
public const TYPE = 'DeletedCustomer';
public function __construct(CustomerId $id)
{
$this->_setId($id);
$this->_setEmail(new Email(sprintf('%s#mail.local', $id->value())));
}
}
The Use Case:
<?php
declare(strict_types=1);
namespace App\Identity\Application\Customer\UseCase\DeleteCustomer;
use App\Identity\Application\Customer\CustomerEntityManager;
use App\Identity\Application\User\AuthenticatedCustomer;
use App\Identity\Domain\Customer\DeletedCustomer;
use App\Shared\Application\ImageManager;
final class DeleteCustomerHandler
{
private CustomerEntityManager $customerEntityManager;
private AuthenticatedCustomer $authenticatedCustomer;
private ImageManager $imageManager;
public function __construct(AuthenticatedCustomer $authenticatedCustomer,
CustomerEntityManager $customerEntityManagerByActiveTenant,
ImageManager $customerPhotoManager)
{
$this->customerEntityManager = $customerEntityManagerByActiveTenant;
$this->authenticatedCustomer = $authenticatedCustomer;
$this->imageManager = $customerPhotoManager;
}
public function handle(): void
{
$customer = $this->authenticatedCustomer->customer();
$photo = (string) $customer->photo();
$deletedCustomer = new DeletedCustomer($customer->id());
// TODO OR return DeletedCustomer that way
// $deletedCustomer = $customer->deactive();
// entityManager->merge() called here
$this->customerEntityManager->sync($deletedCustomer);
// simple entityManager->flush() under the hood
$this->customerEntityManager->update();
// that's a raw query to update discriminator field, hackish way I'm using
// UPDATE customer SET discr = ? WHERE id = ?
$this->customerEntityManager->updateInheritanceType($customer, DeletedCustomer::TYPE);
if ($photo) {
$this->imageManager->remove($photo);
}
}
}
So if you have already an existing Customer persisted and run DeleteCustomerHandler, the Customer will be updated, but its discriminator field won't!
Googling that, there is no way to update the discriminator field not going some hackish way like I do (running raw query manually to update the field).
Also, I need to use the EntityManager->merge() method to add manually initialized DeletedCustomer to internal UnitOfWork. Looks a little bit dirty too, and it's a deprecated method for Doctrine 3, so the question also is there a better way to handle my case?
So, to conclude all the questions:
Am I doing Customer's status change to DeletedCustomer completely wrong? I'm just trying to avoid Customer God Object, distinguish this Entity's bounded contexts, kinda that.
How to avoid EntityManager->merge() there? AuthenticatedCustomer comes from session (JWT).
I think you're absolutely right to want to avoid Customer turning into a god object. Inheritance is one way to do it, but using it for customers in different statuses can lead to problems.
The two key problems in my experience:
As new statuses emerge, will you keep adding different inherited entities?
What happens when you have a customer move through two different statuses, such as a customer that was a NotActivatedCustomer but is now a DeletedCustomer?
So I keep inheritance only when the inherited type is genuinely more specific type, where a given entity will only ever be one of those types for its entire lifecycle. Cars don't become motorbikes, for example.
I have two patterns for solving the problem differently to you. I tend to start with the first and move to the second.
interface DeletedCustomer
{
public function getDeletedAt(): DateTime;
}
interface NotActivatedCustomer
{
public function getCreatedAt(): DateTime;
}
class Customer implements DeletedCustomer, NotActivatedCustomer
{
private $id;
private $name;
private DateTime $deletedAt;
private bool $isActivated = false;
public function getDeletedAt(): DateTime {...}
public function getCreatedAt(): DateTime {...}
}
class DeletedCustomerRepository
{
public function findAll(): array
{
return $this->createQuery(<<<DQL
SELECT customer
FROM Customer
WHERE customer.deletedAt IS NOT NULL
>>>)->getQuery()->getResults();
}
}
class NotActivatedCustomerRepository
{
public function findAll(): array
{
return $this->createQuery(<<<DQL
SELECT customer
FROM Customer
WHERE customer.isActivated = false
>>>)->getQuery()->getResults();
}
}
class DeletedCustomerService
{
public function doTheThing(DeletedCustomer $customer) {}
}
This reduces coupling, which is one of the main problems with god objects. So when the columns start to proliferate, I can move them off to real entities that join to the Customer. Components that refer to DeletedCustomer will still receive one.
The second pattern is event-sourcing-lite - have a many-to-one relationship with a "CustomerLifecycleEvent" entity. Query based on whether the customer has a "deleted" event. This second approach is much more complex, both to update and query. You can still have dedicated repositories that return entities like DeletedCustomer, but you'll need to do a bit more boilerplate.
I was reading this doctrine doc to use custom annotation in my project but I didn't quite undestand what to do, Im kind of dumb
My original idea was to create a formats maker. Something like writting down some text then some variables surrounded by curly braces and then those with the actual value, I want to let my system know which classes can be used to be formatted and which of its properties can the system use.
Lets suppose, in this image, always retrieve the object with id 1 and that id always exists (forgot to draw it)
What I want to do is to have one or more classes with an annotations and then identify which of the classes have that annotation and let me know which classes they are. In the example from above those available classes should be in the dropdown
/*
* #Format
*/
public class Pizza {
public $hasCheese;
/*
* #FormatField("pizzaPrice")
*/
public $price;
Lets assume I have a bunch of classes and the only one that has my annotation Format and some of its properties have the annotation FormatField then I would want to have a way to let me know something like "It looks like the class Pizza can be used in a Format" and then which properties are available for formatting, the ones that have FormatField and then, somehow retrieve the value of that property through an alias pizzaPrice
I wrote this kind of pseudo-code to hopefully better illustrate what I want to learn
var availableClasses = getClassesThatHaveFormatAnnotation();
foreach (availableClasses as availableClass) {
var properties = availableClass.findFormatFields()
print("Looks like the " + availableClass->name + " class has the Format annotation")
foreach (properties as property) {
print("Field available: " + property.name)
print("Value: " + property.value)
}
}
var pizza = findPizzaById(1);
Having this pizza I want to be able to access only the properties that have #FormatField above them through its alias, like {{pizzaPrice}} but {{hasCheese}} should not work because it doesnt have #FormatField annotation;
Here is an example of a code that finds entities and their annotated properties. You can put the result somewhere. But without replacing the annotated entity you will be available all properties, so maybe you need to create a listener, which will listen to symfony kernel event and modify your response data.
$annotationReader = new \Doctrine\Common\Annotations\AnnotationReader();
$entities = [];
foreach ($entities as $entity) {
$reflectionClass = new \ReflectionClass($entity);
if (empty($annotationReader->getClassAnnotation($reflectionClass, CustomAnnotation::class))) {
continue;
}
foreach ($reflectionClass->getProperties() as $property) {
if (empty($annotationReader->getPropertyAnnotation($property, CustomPropertyAnnotation::class))) {
continue;
}
$propertyName = $property->getName();
$propertyValue = $property->getValue($entity);
}
}
I'm trying to count unread messages in laravel app, however my "last_viewed" column contains "raw" time in seconds stored in "last_viewed (int) 11" and my "created_at" contains mysql timestamp so I can't compare them in mysql statement. Here is how I compare them, but it's kind of spaghetti
class Helper {
public static function count_unread_messages()
{
$result = Conversation::join('messages','messages.conversation_id','=','conversation.id')
->join('conversation_members','conversation_members.conversation_id','=','conversation.id')
->select('conversation_members.last_viewed as last_viewed',
'messages.created_at as created_at')
->where('conversation_members.user_id','=',Auth::user()->id)
->groupBy('messages.id')->get();
$i = 0;
foreach ($result as $key) {
if (date($key->last_viewed) < date(strtotime($key->created_at))) {
$i++;
}
}
return $i;
}
}
but I'd like to compare inside Mysql statement, like that
->where('last_viewed' ,'<', 'created_at')->count()
Is there a way to change format of a timestamp into seconds inside a mysql statement?
I think the best way to solve this is to persist time stamps as carbon instances.
You can do that by setting protected $dates = ['last_viewed', 'created_at']; in your Conversation model.
Now when the data get persisted, since last_viewed and created_at persists as carbon instances and mainly because eloquent support carbon, you can simply achieve
->where('last_viewed', '<', 'created_at')->count() functionality.
Hope this was helpful.
Imagine a "Games" class used to track games between opponents. Is it better OOP to have 1 method to retrieve games based on user input parameters or is it better to have multiple methods specific to the retrieval goals?
class Games {
function get_games($game_id = NULL, $stadium_id = NULL, $start_date = NULL,
$end_date = NULL, $count = 999); {}
}
VS
class Games {
function get_all_games($count = 999); {}
function get_game_by_id($game_id = 1); {}
function get_games_by_stadium($stadium_id = 1); {}
function get_games_by_dates($start_date = NULL; $end_date = NULL) {}
}
Explanation of benefits and any coding / snytax tips would be appreciated. Thanks.
The more I practice OOP the more I find myself following a rule about passing parameters to methods. Kind of like having many levels of nested if statements, I find that if I have more than two I might be doing something wrong.
Keep your code simple. You're writing a method that does something, not a block of procedural code that does everything. If you want to get a game, then get a game. If you want to get a list for a date range, then do that.
However I would point out that you don't really need get_all_games() - You can just allow for get_games_by_dates() to be passed with no parameters. If it doesn't get any then it would get the games for every date since forever (all the games)
I would always err on the side of OOP code. the reason being is that it makes you code much easier to maintain and read. The more functions you have the easier it is to follow code later on down the road
I would go for separate methods since you are using lots of paramaters with default values.
If you want to get all games you would have to do:
$games->get_games(NULL, NULL, NULL, NULL, 999);
Assuming that your get_....() functions are returning all game data, I would write a single function to return this data, based on an id passed in, and write a series of find_...() functions to return an array of found ids. This will have the added benefit of making it easier to override the data retrieval code in decendant classes.
class Games {
public function get_game($game_id) {
// Return game details (array/object) for $game_id, or FALSE if not found.
}
public function find_all_games() {
// Return array of ids for all games.
}
public function find_games_by_dates($start_date = NULL, $end_date = NULL) {
// Return array of ids between $start_date and $end_date unless NULL.
}
}
You can then call:
$oGames = new Games() ;
$aGames = $oGames->find_all_games() ;
foreach($aGames as $id) {
$aGame = $oGames->get_game($id) ;
if($aGame !== FALSE) { // This check might be skipped if you trust the array of ids from find_all_games().
// Assuming an array is returned.
echo "Game Found: ".$aGame['name']."\n" ;
}
}
The benefit of "multiple methods specific to the retrieval goals" is that you can add/remove goals. The problem with using one monolithic function with a bunch of parameters is that, should you decide to add/remove a way to get games, you'd have to change the interface. Which would break any code that uses it.
Each method should be as concise as possible, performing only one function.
What is the best way of working with calculated fields of Propel objects?
Say I have an object "Customer" that has a corresponding table "customers" and each column corresponds to an attribute of my object. What I would like to do is: add a calculated attribute "Number of completed orders" to my object when using it on View A but not on Views B and C.
The calculated attribute is a COUNT() of "Order" objects linked to my "Customer" object via ID.
What I can do now is to first select all Customer objects, then iteratively count Orders for all of them, but I'd think doing it in a single query would improve performance. But I cannot properly "hydrate" my Propel object since it does not contain the definition of the calculated field(s).
How would you approach it?
There are several choices. First, is to create a view in your DB that will do the counts for you, similar to my answer here. I do this for a current Symfony project I work on where the read-only attributes for a given table are actually much, much wider than the table itself. This is my recommendation since grouping columns (max(), count(), etc) are read-only anyway.
The other options are to actually build this functionality into your model. You absolutely CAN do this hydration yourself, but it's a bit complicated. Here's the rough steps
Add the columns to your Table class as protected data members.
Write the appropriate getters and setters for these columns
Override the hydrate method and within, populate your new columns with the data from other queries. Make sure to call parent::hydrate() as the first line
However, this isn't much better than what you're talking about already. You'll still need N + 1 queries to retrieve a single record set. However, you can get creative in step #3 so that N is the number of calculated columns, not the number of rows returned.
Another option is to create a custom selection method on your TablePeer class.
Do steps 1 and 2 from above.
Write custom SQL that you will query manually via the Propel::getConnection() process.
Create the dataset manually by iterating over the result set, and handle custom hydration at this point as to not break hydration when use by the doSelect processes.
Here's an example of this approach
<?php
class TablePeer extends BaseTablePeer
{
public static function selectWithCalculatedColumns()
{
// Do our custom selection, still using propel's column data constants
$sql = "
SELECT " . implode( ', ', self::getFieldNames( BasePeer::TYPE_COLNAME ) ) . "
, count(" . JoinedTablePeer::ID . ") AS calc_col
FROM " . self::TABLE_NAME . "
LEFT JOIN " . JoinedTablePeer::TABLE_NAME . "
ON " . JoinedTablePeer::ID . " = " . self::FKEY_COLUMN
;
// Get the result set
$conn = Propel::getConnection();
$stmt = $conn->prepareStatement( $sql );
$rs = $stmt->executeQuery( array(), ResultSet::FETCHMODE_NUM );
// Create an empty rowset
$rowset = array();
// Iterate over the result set
while ( $rs->next() )
{
// Create each row individually
$row = new Table();
$startcol = $row->hydrate( $rs );
// Use our custom setter to populate the new column
$row->setCalcCol( $row->get( $startcol ) );
$rowset[] = $row;
}
return $rowset;
}
}
There may be other solutions to your problem, but they are beyond my knowledge. Best of luck!
I am doing this in a project now by overriding hydrate() and Peer::addSelectColumns() for accessing postgis fields:
// in peer
public static function locationAsEWKTColumnIndex()
{
return GeographyPeer::NUM_COLUMNS - GeographyPeer::NUM_LAZY_LOAD_COLUMNS;
}
public static function polygonAsEWKTColumnIndex()
{
return GeographyPeer::NUM_COLUMNS - GeographyPeer::NUM_LAZY_LOAD_COLUMNS + 1;
}
public static function addSelectColumns(Criteria $criteria)
{
parent::addSelectColumns($criteria);
$criteria->addAsColumn("locationAsEWKT", "AsEWKT(" . GeographyPeer::LOCATION . ")");
$criteria->addAsColumn("polygonAsEWKT", "AsEWKT(" . GeographyPeer::POLYGON . ")");
}
// in object
public function hydrate($row, $startcol = 0, $rehydrate = false)
{
$r = parent::hydrate($row, $startcol, $rehydrate);
if ($row[GeographyPeer::locationAsEWKTColumnIndex()]) // load GIS info from DB IFF the location field is populated. NOTE: These fields are either both NULL or both NOT NULL, so this IF is OK
{
$this->location_ = GeoPoint::PointFromEWKT($row[GeographyPeer::locationAsEWKTColumnIndex()]); // load gis data from extra select columns See GeographyPeer::addSelectColumns().
$this->polygon_ = GeoMultiPolygon::MultiPolygonFromEWKT($row[GeographyPeer::polygonAsEWKTColumnIndex()]); // load gis data from extra select columns See GeographyPeer::addSelectColumns().
}
return $r;
}
There's something goofy with AddAsColumn() but I can't remember at the moment, but this does work. You can read more about the AddAsColumn() issues.
Here's what I did to solve this without any additional queries:
Problem
Needed to add a custom COUNT field to a typical result set used with the Symfony Pager. However, as we know, Propel doesn't support this out the box. So the easy solution is to just do something like this in the template:
foreach ($pager->getResults() as $project):
echo $project->getName() . ' and ' . $project->getNumMembers()
endforeach;
Where getNumMembers() runs a separate COUNT query for each $project object. Of course, we know this is grossly inefficient because you can do the COUNT on the fly by adding it as a column to the original SELECT query, saving a query for each result displayed.
I had several different pages displaying this result set, all using different Criteria. So writing my own SQL query string with PDO directly would be way too much hassle as I'd have to get into the Criteria object and mess around trying to form a query string based on whatever was in it!
So, what I did in the end avoids all that, letting Propel's native code work with the Criteria and create the SQL as usual.
1 - First create the [get/set]NumMembers() equivalent accessor/mutator methods in the model object that gets returning by the doSelect(). Remember, the accessor doesn't do the COUNT query anymore, it just holds its value.
2 - Go into the peer class and override the parent doSelect() method and copy all code from it exactly as it is
3 - Remove this bit because getMixerPreSelectHook is a private method of the base peer (or copy it into your peer if you need it):
// symfony_behaviors behavior
foreach (sfMixer::getCallables(self::getMixerPreSelectHook(__FUNCTION__)) as $sf_hook)
{
call_user_func($sf_hook, 'BaseTsProjectPeer', $criteria, $con);
}
4 - Now add your custom COUNT field to the doSelect method in your peer class:
// copied into ProjectPeer - overrides BaseProjectPeer::doSelectJoinUser()
public static function doSelectJoinUser(Criteria $criteria, ...)
{
// copied from parent method, along with everything else
ProjectPeer::addSelectColumns($criteria);
$startcol = (ProjectPeer::NUM_COLUMNS - ProjectPeer::NUM_LAZY_LOAD_COLUMNS);
UserPeer::addSelectColumns($criteria);
// now add our custom COUNT column after all other columns have been added
// so as to not screw up Propel's position matching system when hydrating
// the Project and User objects.
$criteria->addSelectColumn('COUNT(' . ProjectMemberPeer::ID . ')');
// now add the GROUP BY clause to count members by project
$criteria->addGroupByColumn(self::ID);
// more parent code
...
// until we get to this bit inside the hydrating loop:
$obj1 = new $cls();
$obj1->hydrate($row);
// AND...hydrate our custom COUNT property (the last column)
$obj1->setNumMembers($row[count($row) - 1]);
// more code copied from parent
...
return $results;
}
That's it. Now you have the additional COUNT field added to your object without doing a separate query to get it as you spit out the results. The only drawback to this solution is that you've had to copy all the parent code because you need to add bits right in the middle of it. But in my situation, this seemed like a small compromise to save all those queries and not write my own SQL query string.
Add an attribute "orders_count" to a Customer, and then write something like this:
class Order {
...
public function save($conn = null) {
$customer = $this->getCustomer();
$customer->setOrdersCount($customer->getOrdersCount() + 1);
$custoner->save();
parent::save();
}
...
}
You can use not only the "save" method, but the idea stays the same. Unfortunately, Propel doesn't support any "magic" for such fields.
Propel actually builds an automatic function based on the name of the linked field. Let's say you have a schema like this:
customer:
id:
name:
...
order:
id:
customer_id: # links to customer table automagically
completed: { type: boolean, default false }
...
When you build your model, your Customer object will have a method getOrders() that will retrieve all orders associated with that customer. You can then simply use count($customer->getOrders()) to get the number of orders for that customer.
The downside is this will also fetch and hydrate those Order objects. On most RDBMS, the only performance difference between pulling the records or using COUNT() is the bandwidth used to return the results set. If that bandwidth would be significant for your application, you might want to create a method in the Customer object that builds the COUNT() query manually using Creole:
// in lib/model/Customer.php
class Customer extends BaseCustomer
{
public function CountOrders()
{
$connection = Propel::getConnection();
$query = "SELECT COUNT(*) AS count FROM %s WHERE customer_id='%s'";
$statement = $connection->prepareStatement(sprintf($query, CustomerPeer::TABLE_NAME, $this->getId());
$resultset = $statement->executeQuery();
$resultset->next();
return $resultset->getInt('count');
}
...
}