Custom ORM expressions - unable to call

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • bandtank
    Active Community Member
    • Mar 2017
    • 435

    #1

    Custom ORM expressions - unable to call

    I am writing a new ORM function to convert timezones by name. However, I can't figure out how to implement the instructions here regarding the creation of a custom function.

    custom/Espo/Modules/Odyssey/Resources/metadata/app/orm.json
    Code:
    {
        "platforms": {
            "Mysql": {
                "functionConverterClassNameMap": {
                  "convertTimezoneByName": "Espo\\Modules\\Odyssey\\Classes\\ORM\\ConvertTimezoneByName"
                }
            }
        }
    }
    custom/Espo/Modules/Odyssey/Classes/ORM/ConvertTimezoneByName.php
    PHP Code:
    <?php
    
    namespace Espo\Modules\Odyssey\Classes\ORM;
    
    use Espo\ORM\QueryComposer\Part\FunctionConverter;
    
    class ConvertTimezoneByName implements FunctionConverter
    {
      public function convert(string ...$argumentList): string
      {
        ...
    
        return "CONVERT_TZ({$datetime}, {$fromTz}, {$toTz})";
      }
    }

    It doesn't work when used in an ORM expression:
    PHP Code:
      private function getQueryBuilder(?SearchParams $searchParams, ?User $user): QueryBuilder
      {
        ...
    
        $queryBuilder = $selectBuilder->buildQueryBuilder();
    
        $queryBuilder->where(
          Expr::or(
            Expr::notEqual(
              Expr::date(Expr::convertTimezoneByName(Expr::column('dateStart'), 'America/Denver')),
              Expr::date(Expr::convertTimezoneByName(Expr::column('dateStartBill'), 'America/Denver')),
            ),
          )
        );
    
        ...
      }
    Code:
    ... ERROR: Slim Application Error Type: Error Code: 0 Message: Call to undefined
    method Espo\ORM\Query\Part\Expression::convertTimezoneByName()
    I've tried probably 20 different ways to call the function. Nothing works. Any advice would be greatly appreciated.
  • bandtank
    Active Community Member
    • Mar 2017
    • 435

    #2
    Notably, the function is also not available in the GUI:
    Click image for larger version

Name:	CleanShot 2026-06-16 at 09.37.21.png
Views:	0
Size:	9.6 KB
ID:	126814

    Comment

    • yuri
      EspoCRM product developer
      • Mar 2014
      • 9862

      #3
      Expr::convertTimezoneByName won't work, as the Expression is a static class. It cannot be patched dynamically which is dictated by the PHP language design.


      Here's an example:



      You need to use UPPER_CASE when defining in metadata.

      Usage in PHP code:

      PHP Code:
      Expression::create('CONVERT_TIMEZONE_BY_NAME:(myColumn, "America/Denver")') 
      

      Note that the column argument is not available in the function implementation. The real value will be resolved at the database level.

      Another example: https://github.com/espocrm/espocrm/b...cyRate.php#L49

      Comment


      • yuri
        yuri commented
        Editing a comment
        You can just compose the needed database function call string, passing argumentList[0] and argumentList[1] into its parameters.
    • yuri
      EspoCRM product developer
      • Mar 2014
      • 9862

      #4
      Complex expression field autocomplete is extended separately: https://docs.espocrm.com/development...ex-expression/

      Comment

      • bandtank
        Active Community Member
        • Mar 2017
        • 435

        #5
        EDIT: I submitted this response before I saw your comments. Thanks for the help. I will review the information now.

        Some progress has been made, but there are still many unknowns because this situation is not explained in the documentation. I shortened the name of the custom function to TZ2 to make things easier.

        Here is the full definition of the function:

        custom/Espo/Modules/Odyssey/Classes/ORM/ConvertTimezoneByName.php
        PHP Code:
        <?php
        
        namespace Espo\Modules\Odyssey\Classes\ORM;
        
        use Espo\ORM\QueryComposer\Part\FunctionConverter;
        
        class ConvertTimezoneByName implements FunctionConverter
        {
          public function convert(string ...$argumentList): string
          {
            $count = count($argumentList);
        
            if($count < 2 || $count > 3) {
              throw new RuntimeException("CONVERT_TIMEZONE_BY_NAME requires 2 or 3 arguments, {$count} provided.");
            }
        
            $datetime = $argumentList[0];
        
            if($count === 2) {
              $fromTz = "'UTC'";
              $toTz = $argumentList[1];
            } else {
              $fromTz = $argumentList[1];
              $toTz = $argumentList[2];
            }
        
            return "CONVERT_TZ({$datetime}, {$fromTz}, {$toTz})";
          }
        }

        Building an expression like this compiles correctly:
        Code:
        Expr::date(Expr::create("TZ2:('dateStart','America/Denver')"));
        Here is the resulting SQL:
        Code:
        DATE(CONVERT_TZ('dateStart', 'UTC', 'America/Denver')
        The column name was not converted to the right format: date_start. For some reason, using:
        Code:
        Expr::date(Expr::convertTimezone(Expr::column('dateStart'), -6));
        produces this SQL:
        Code:
        DATE(CONVERT_TZ(session.date_start, '+00:00', '-06:00')))
        I can't figure out how to get Espo to compile my version into session.date_start instead of dateStart.
        Last edited by bandtank; Today, 05:37 PM.

        Comment

        • yuri
          EspoCRM product developer
          • Mar 2014
          • 9862

          #6
          You pass it as a literal string 'dateStartBill'. You need to pass it without quotes.


          PHP Code:
          Expr::create("TZ2:(dateStartBill, 'America/Denver')") 
          

          Comment

          • bandtank
            Active Community Member
            • Mar 2017
            • 435

            #7
            Thank you for the help. It works now. Here are the final implementations in case anyone else would like to do something similar.

            custom/Espo/Modules/Odyssey/Resources/metadata/app/orm.json
            Code:
            {
                "platforms": {
                    "Mysql": {
                        "functionConverterClassNameMap": {
                          "TZ2": "Espo\\Modules\\Odyssey\\Classes\\ORM\\ConvertTimezoneByName"
                        }
                    }
                }
            }
            custom/Espo/Modules/Odyssey/Classes/ORM/ConvertTimezoneByName.php
            PHP Code:
            <?php
            
            namespace Espo\Modules\Odyssey\Classes\ORM;
            
            use Espo\ORM\QueryComposer\Part\FunctionConverter;
            
            class ConvertTimezoneByName implements FunctionConverter
            {
              public function convert(string ...$argumentList): string
              {
                $count = count($argumentList);
            
                if($count < 2 || $count > 3) {
                  throw new RuntimeException("CONVERT_TIMEZONE_BY_NAME requires 2 or 3 arguments, {$count} provided.");
                }
            
                $datetime = $argumentList[0];
            
                if($count === 2) {
                  $fromTz = "'UTC'";
                  $toTz = $argumentList[1];
                } else {
                  $fromTz = $argumentList[1];
                  $toTz = $argumentList[2];
                }
            
                return "CONVERT_TZ({$datetime}, {$fromTz}, {$toTz})";
              }
            }
            Example usage in the backend:
            PHP Code:
              private function getQueryBuilder(?SearchParams $searchParams, ?User $user): QueryBuilder
              {
                $selectBuilder = $this->selectBuilderFactory
                  ->create()
                  ->from(CSession::ENTITY_TYPE)
                  ->withStrictAccessControl()
                  ->withSearchParams($searchParams);
            
                if($user)
                  $selectBuilder->forUser($user);
            
                $queryBuilder = $selectBuilder->buildQueryBuilder();
            
                $queryBuilder->where(
                  Expr::or(
                    Expr::notEqual(
                      Expr::date(Expr::create("TZ2:(dateStart    , 'America/Denver')")),
                      Expr::date(Expr::create("TZ2:(dateStartBill, 'America/Denver')")),
                    ),
                    Expr::notEqual(
                      Expr::date(Expr::create("TZ2:(dateEnd      , 'America/Denver')")),
                      Expr::date(Expr::create("TZ2:(dateEndBill  , 'America/Denver')")),
                    ),
                  )
                );
            
                return $queryBuilder;
              } 
            
            custom/Espo/Modules/Odyssey/Resources/metadata/app/complexExpression.json
            Code:
            {
                "functionList": [
                    "__APPEND__",
                    {
                        "name": "TZ2_UTC",
                        "insertText": "TZ2:(COL, TZ_TO)",
                        "returnType": "string"
                    },
                    {
                        "name": "TZ2",
                        "insertText": "TZ2:(COL, TZ_FROM, TZ_TO)",
                        "returnType": "string"
                    }
                ]
            }
            which results in this:

            Click image for larger version  Name:	CleanShot 2026-06-16 at 11.19.59.png Views:	0 Size:	6.1 KB ID:	126823

            My report uses the function like this:
            Code:
            OR:(
              NOT_EQUAL:(
                DAYOFMONTH:(TZ2:(
                  dateStart,
                  'America/Denver'
                )),
                DAYOFMONTH:(TZ2:(
                  dateStartBill,
                  'America/Denver'
                ))
              ),
              NOT_EQUAL:(
                DAYOFMONTH:(TZ2:(
                  dateEnd,
                  'America/Denver'
                )),
                DAYOFMONTH:(TZ2:(
                  dateEndBill,
                  'America/Denver'
                 ))
              )
            )​


            I still need to replace DAYOFMONTH with DATE.


            EDIT: Actually, DATE works already, but there is no autocomplete. You can fix that by adding an entry for the function in the complexExpression.json file​:

            Code:
            {
                "functionList": [
                    ...
                    {
                        "name": "DATE",
                        "insertText": "DATE:(VALUE)",
                        "returnType": "string"
                    }
                ]
            }​
            Last edited by bandtank; Today, 05:34 PM.

            Comment

            Working...