NetPhp User Guide

NetPhp User Guide

Difficulty: 
Piece of Cake

This is the User Guide for the 2.x series of the NetPhp component.

What is NetPhp

The NetPhp libraries are split into two main components:

Historically, consuming .Net assemblies from PHP is done by means of the com_dotnet extension (included in PHP core).

The problem of using the extension as-is is that it is there are a big number of issues and rigidity when consuming your assemblies. For example, you are limited to using .Net 3.5 or lower compiled assemblies, and any binary you want to consume need to be decorated as COM Visible (so you need to recompile and interface anything you plan to use).

With NetPhp all those restrictions have been overcome, allowing you to consume any type of compiled .Net library (no mater what framework version) giving flexibility as to how is the type loaded where you can even specify the exact location of the dynamic link library to get it loaded real time.

You can even generate a PHP class model of your .Net binaries (of the .Net framework itself) and unleash all the power of your IDE.

​Deploying the NetPhp binary

The NetPhp binary is the the bridge between the PHP com_dotnet extension and any .Net framework based binary. This is how it works:

  • Your code talks to the NetPhp PHP classes
  • The NetPhp PHP classes use the COM_DOTNET core extension to instantiate and interact with the NetPhp binary.
  • The NetPhp binary is a Reflection based door to any .Net framework binary in your system

There are 2 different ways of deploying the binary:

  • (1) Registering it as a system wide COM component
  • (2) Registering it in as a .Net binary either in the GAC or in a location that is discoverable by the .Net framework

This is because PHP has two different ways of loading the NetPhp binary: using the COM class or using the DOTNET class. Using the former loading strategy you will be able to load a library targeted at ANY version of the .Net framework, while the latter will limit you .Net loading capabilities to only using binaries targete for CLR2 or before (.Net Framework 3.5 o below).

That is why the recomended deployment strategy is by registering the binary as a COM component.

As a COM component

The first thing you need to do is download the compiled binary for the highest version of the .Net framework available, in this case the .Net 4.6 framework from here

Once downloaded place the DLL file somewhere in your system that you will not forget about such as

D:\netphp\netutilities.dll

Registering the binary as a COM component means that you will make the whole system aware of where this dll has been deployed. 

Next we are going to use the regasm tool to register our COM classes:

"C:\Windows\Microsoft.NET\Framework\v4.0.30319\regasm.exe" netutilities.dll /codebase

Notice that we are registering our binary against the x86 version of the .Net framework. Unless you are using a 64 bit build of PHP, this is the way to go.

Whenever you upgrade your NetPhp binary to a new version make sure to first unregister the previous DLL:

"C:\Windows\Microsoft.NET\Framework\v4.0.30319\regasm.exe" /u netutilities.dll

Or you will run into what we call a dependency hell.....

To make sure you have succeded registering your COM components download OleView and run the x86 version of OleView.exe. Ole view is part of the Windows SDK, but we have packed just the binary in the referenced download.

You will se your COM classes inside the ".Net Category" group:

As a DOTNET assembly

If registering a system wide COM component is not an option for you due to IT security policies or other restrictions, you can register your assemblies in the GAC.

"C:\Windows\Microsoft.NET\Framework\v2....\gacutil.exe" -i netutilities.dll

If doing so, you will need to load the binaries using PHP's DOTNET class instead of PHP's COM class:

$runtime = new \NetPhp\Core\NetPhpRuntime('DOTNET', 'ASSEMBLY FULL QUALIFIED NAME OR WEAK NAME');
$runtime->Initialize();

If you use this deployment strategy, you will be limited to PHP's DOTNET handling of CLR2. You will only be able to deal with .Net assemblies compiled for any version equal or prior to .Net 3.5. Of course, you must use the CLR2 netutilities binary and register it in the CLR2 version of the framework in your system.

Adding the NetPhp classes to your project

The next step is to add the NetPhp classes to your current or new PHP project. The easiest way to accomplish this is using composer.

Add - if you don't have already - a composer.json file to the root of your project such as this one:

{
    "require": {
        "drupalonwindows/netphp": "2.x-dev"
    },
    "autoload": {
        "psr-4": {
            "MyProject\\": "src/",
        }
    }
}

Run composer install or composer update to retrieve the packages into your project and to generate the required autoload files.

If you get lost here make sure to check out the step by step example from this user guide. Understading how composer and autoloading works in PHP is crucial to building succesful PHP projects.

Preparing the NetPhp runtime

Initializing the NetPhp runtime is a heavy task that should only be done once by request. You should create a runtime object using a singleton or service pattern, and reuse it throughout the request.

Initialization consists in telling NetPhp what .Net assemblies you are going to use so that it can preload them, make sure that dependencies can be discovered and examine all the Types in the binaries so that you can easily use them through the request.

For demonstration purposes we are going to prepare a procedural sample:

// Instantiate and initialize the Model
$runtime = new \NetPhp\Core\NetPhpRuntime('COM', 'netutilities.NetPhpRuntime');
$runtime->Initialize();

// Register the assemblies that we will be using. The runtime has bundles with some
// of the most used assemblies of the .Net framework.
$runtime->RegisterNetFramework2();

// Add both SpreadsheetLight and the OpenXML it depends on.
$runtime->RegisterAssemblyFromFile(APPLICATION_ROOT . '/binaries/SpreadsheetLight.dll', 'SpreadsheetLight');
$runtime->RegisterAssemblyFromFile(APPLICATION_ROOT . '/binaries/DocumentFormat.OpenXml.dll', 'DocumentFormat.OpenXml');
$runtime->RegisterAssemblyFromFile(APPLICATION_ROOT . '/binaries/AjaxMin.dll', 'AjaxMin');

The first thing is to instantiate a NetPhpRuntime, we can do this in 3 different ways.

Using the COM Program Id:

$runtime = new \NetPhp\Core\NetPhpRuntime('COM', 'netutilities.NetPhpRuntime');

Using the COM Class Id. Use this if you are having trouble using the Program Id (usually because you inproperly registered different versions of NetPhp and now COM is not able to resolve the Program Id to a specific DLL). You can get the Class Id from OleView, or looking at the dockblocks of the NetPhpRuntime class.

$runtime = new \NetPhp\Core\NetPhpRuntime('COM', '{2BF990C8-2680-474D-BDB4-65EEAEE0015F}');

Using the DOTNET full qualified name. If you use this method you need the CLR2 version of the assembly that must be copied to your PHP.exe directory or registered in the GAC. Remember that by using this load mode you are limited to the .Net framework 3.5 or below.

$runtime = new \NetPhp\Core\NetPhpRuntime('DOTNET', 'netutilities, Version=2.0.0.0, Culture=neutral, PublicKeyToken=eb7e88f4dd476502');

The next Step is to register the assemblies we will be using:

// Add both SpreadsheetLight and the OpenXML it depends on.
$runtime->RegisterAssemblyFromFile(APPLICATION_ROOT . '/binaries/SpreadsheetLight.dll', 'SpreadsheetLight');
$runtime->RegisterAssemblyFromFile(APPLICATION_ROOT . '/binaries/DocumentFormat.OpenXml.dll', 'DocumentFormat.OpenXml');
$runtime->RegisterAssemblyFromFile(APPLICATION_ROOT . '/binaries/AjaxMin.dll', 'AjaxMin')

Make sure that these point to valid assemblies that are compatible with the CLR limitations mentioned before.

You are now ready to start using this runtime, you can do 2 things now:

  • Use the runtime "on the fly" without a PHP class model.
  • Dump a PHP class model of the runtime so that you can interact with PHP classes that represent the .Net types.

Consuming .Net binaries on the fly

Once you have your runtime you can start operating on .Net types. The first thing you need to ask the runtime for a "Type" representation of the classes you want to instantiate or call static methods or Enums. There are 3 ways of doing so:

// Using the FullName of a type that belongs to an assembly that has already been registered.
$datetime = $runtime->TypeFromName("System.DateTime");

// Using the FullName of a type that has not been registered yet (from a file)
$minifier = $runtime->TypeFromFile("Microsoft.Ajax.Utilities.Minifier", APPLICATION_ROOT . '/binaries/AjaxMin.dll');

// Using the FullName of a type that has not been registered yet (autodiscoverable)
$datetime2 = $runtime->TypeFromAssembly("System.DateTime", "mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
  

Instantiating Objects

These objects returned by the Framework represent a .Net type. If you really want to instantiate the .Net type create an instance by calling the Instantiate() method:

$datetime->Instantiate();
echo $datetime->ToShortDateString()->Val(); // Outputs 01/01/0001

Calling Instantiate() is like calling the constructor, so you can pass in any parameteres you wish as long as they match any of the constructor overloads:

// We can only use Int32 from native PHP, so parse
// an Int64 that is equivalent to (long) in the DateTime constructor.
$ticks = $runtime->TypeFromName("System.Int64")->Parse('98566569856565656');

$datetime->Instantiate($ticks);
echo $datetime->ToShortDateString()->Val(); // Outputs 07/05/0313

Using Properties

You can access static properties directly on the non instantiated wrappers:

$now = $runtime->TypeFromName("System.DateTime")->Now;
echo $now->ToShortDateString()->Val(); // Outputs "23/10/2015"

$now = $runtime->TypeFromName("System.DateTime")->Now();
echo $now->ToShortDateString()->Val(); // Outputs "23/10/2015"

Notice that you can operate with Properties in two ways: as if they were PHP properties, or by calling a method with their same name. Setting the value of a property is equally easy:

$timer = $runtime->TypeFromName("System.Timers.Timer")->Instantiate();
$timer->AutoReset(TRUE);
$timer->AutoReset = TRUE;

Calling Methods

Calling methods can be done imitating a method call on the PHP object:

$datetime->AddYears(25);

Mixing .Net and PHP types

At any time you can operate with .Net objects using PHP native types, keeping in mind that they will be converted to their .Net siblings. The mappings are as follows:

  • int : System.Int32 (System.Int64 for x64 builds of PHP)
  • float: System.Single
  • boolean :. System.Boolean
  • string : System.String

Every call to a method, function, property or any other interaction with the .Net types will return a Wrapper. Call the method Val() on the wrapper to retrieve the native PHP type as long as it is convertible.

// Result here is a .Net string but not a PHP string
$result = $now->ToShortDateString();
// Use VAL to retrieve the internal .Net type
$php_result = $result->Val()

To ease the interaction between .Net and PHP you can call the GetPhpFromJson() method on any Wrapper and what will happen is that .Net will try to serialize the object into JSON, pass it to PHP, and then json_decode it so you can use it:

$data = $timer->GetPhpFromJson();

var_dump($data);

    // Outputs:
    //  object(stdClass)[38]
    //     public 'AutoReset' => boolean true
    //     public 'Enabled' => boolean false
    //     public 'Interval' => int 100
    //     public 'Site' => null
    //     public 'SynchronizingObject' => null
    //     public 'Container' => null

This will allow you to virtually retrieve anything from the .Net realm into PHP. 

Upgrading to PHP 7 or x64 versions of PHP

When you upgrade to x64 versions of PHP there are several things you should consider:

  • COM x86 and COM x64 are independant. You will need to register the NetPhp binary on COM x64.
  • Type mapping changes. What used to arrive as an Int32 to .Net will now be an Int64. This makes call to methods that take Int32 (or regular Int) as parameters incompatible with the integers that come from PHP. This is NOT  a limitation of NetPhp, if you try to send an Int64 to a method declared with an Int32 you will not be able to compile in .Net. Because boxing an Int64 inside an Int32 has a risk of data loss, NetPhp does not have (but could) automatic Int64 to Int32 parameter conversion. You can workaround this issue from your PHP code like this:
// Get the System.Convert Type
$convert = $runtime->TypeFromName('System.Convert');

//  This is an Int64 when PHP sends it to COM/DOTNET
$h = (int) 645;

// The second parameter is declared with an Int32, just do a conversion before sending the paramter.
$pdfFile->AddImageUrl($url, FALSE, $convert->ToInt32($h), TRUE);

Using Enums

Using an enum is as easy as calling Enum() on a Wrapper the represent an Enum .Net type.

In this example we check if today is Monday, the .Net code would be:

var IsMonday = System.DateTime.Now.DayOfWeek == System.DayOfWeek.Monday;

To convert this to PHP we will use this:

$IsMonday = $runtime->TypeFromName("System.DateTime")->Now->DayOfWeek->Equals($runtime->TypeFromName("System.DayOfWeek")->Enum('Monday'));

Notice that we CANNOT directly compare using the PHP equals (==) operator because this will compare our wrappers, not the objects themselves. In exchange, we rely on the Equals() method that is available for any .Net type.

Working with collections

When a wrapper contains an object that has any collection related behaviour (IEnumerable, IList, etc..) on .Net the framework will automatically provide the PHP required wrappers to operate on these collections directly from PHP.

We can create an ArrayList like this:

$list = $runtime->TypeFromName("System.Collections.ArrayList")->Instantiate()->AsIterator();

Notice the call to AsIterator(). You can call this method on any Wrapper and you will enable the collection related functionalities. Of course, this only works if the wrapped object or type on the .Net side implements the required interfaces and methods (Countable, IEnumerable, etc.).

Now add some elements to the ArrayList and output the count:

$list->Add('My first thing');
$list->Add(2562);

echo count($list); // Outputs 2

You can iterate on this object as if it was a PHP array or collection:

foreach ($list as $item) {
 echo "item: {$item->Val()} </br>";
}

// Outputs:
//
// item: My first thing 
// item: 2562 

Consuming .Net binaries with a PHP based class model

One of the key features of NetPhp 2.x is the ability go generate a PHP class model that representes the .Net types in your assemblies. 

So instead of dealing with wrappers:

$ticks = $runtime->TypeFromName("System.Int64")->Parse('98566569856565656');

You will be interacting with .Net as if it were a set of PHP classes:

$ticks = \ms\System\netInt64::_Parse('98566569856565656');

By using a PHP class model:

  • You get more readable code
  • Code is less error prompt
  • You get IDE autocomplete
  • You get Type validation for parameters

Dumping the PHP class model

To dump the class model you use the TypeDumper class.

// Initialize the dumper.
$dumper = new \NetPhp\Core\TypeDumper();
$dumper->Initialize();

You need to tell the dumper where to dump the class model and what namespace to use. This is critical to get autoloading to work properly.

$dumper->SetDestination(APPLICATION_ROOT . '/ms');
$dumper->SetBaseNamespace('ms');

You should make these details match your autoloading configuration. For the previous example and if you are using the Composer generated autoloader this will be your matching composer.json:

{
    "autoload": {
        "psr-4": {
            "ms\\": "ms/",
        }
    }
}

The next step is to tell the dumper what assemblies you are going to use for the dump. If you have an already setup NetPhp runtime you can tell the runtime to register the binaries in the dumper:

// Get a copy of the runtime.
$runtime = $this->GetRuntime();

// Tell the runtime to register the assemblies in the Dumper.
$runtime->RegisterAssembliesInDumper($dumper);

You can manually register individual assemblies in the dumper:

$dumper->RegisterAssemblyFromFileName('d:\\path-to-my\\file.dll');
$dumper->RegisterAssemblyFromFullQualifiedName('mscorlib, .....');

Because dumping everything inside the assemblies can yield quite a big class model, the next step is to tell the dumper what .Net types it should dump.

To do so you register a set of regular expression based filters. Any type whose Full Name matches any of the registered regular expressions will be dumped:

// Only dump the SpreadshettLight namespace.
$dumper->AddDumpFilter('^SpreadsheetLight\..*');

// Add a few hand picked classes.
$dumper->AddDumpFilter('^System\.Convert$');
$dumper->AddDumpFilter('^System\.DateTime$');
$dumper->AddDumpFilter('^Microsoft\.Ajax\.Utilities.*');

Note: the dumper will only consider Types that have been registered into it. If you expect the System.DateTime class to be dumped, yet you have not registered mscorlib into the dumper, it will not get dumped.

To make the dumps usable and easy to do, the dumper will inspect the set of types that result of applying the registered filter rules and analyze recursively the types that appear in parameters, constructors, properties, fields, interfaces, etc. so that it will dump a comprehensive set of types to make the model usable.

To specify the maximum recursion depth you use the SetDepth() method:

$dumper->SetDumpDepth(1);

To prevent generaton of inconsistent class models, the dumper will erase all the contents in the destination directory. As a safety measure you need to explictly call AllowDestinationDirectoryClear() on the dumper to allow it to delete anything:

$dumper->AllowDestinationDirectoryClear();

You are now ready to generate the PHP class model:

// Generate the static class model.
try {
  $dumper->GenerateModel();
  // Just make sure that PHP can start using this new classes
  // during this same request.
  clearstatcache();
}
catch (\Exception $e){}

Generating this model can take quite a while, depending on the number of dumped types and assemblies. On extreme cases we have seen this take up to a minute.

Using the PHP class model

The generated class model will be using the designated base namespace and will prefix al .Net type names with "net".

So if you used the namespace prefix "ms" for you dump, the System.Date time class will be available at \ms\System\netDateTime.

The dumper is prefixing all type names to prevent collisions with PHP reserved words.

Before being able to use your static class model, you asign the model a NetPhp runtime to use. To do so you need to use the TypeMap class created at the root of the namespace:

\ms\TypeMap::SetRuntime($this->runtime);

To instantiate a class look for a static method on the PHP type with the class name and the suffix "_constructor":

$datetime = \ms\System\netDateTime::DateTime_Constructor();

You will find several particularities in the PHP model when comparing them with their original .Net types:

  • All type names are prefixed with "net".
  • Static methods are prefixed with an underscore "_".
  • Properties are exposed as methods.

License and Activation

See the licensing options here.

The evaluation version is fully functional but will stop working on sundays, Christmas, New Year, and every day between 2:00H and 5:00H. It will also throw random exceptions making it unfeasible to use on production environments.

Before being able to use any license related operations, you need to tell the component WHERE to store the activation data:

$runtime = new \NetPhp\Core\NetPhpRuntime();
$runtime->Initialize();

$license_path = 'd:\\netphp\\license.txt'; 
$runtime->ActivationLicenseInitialize($license_path);

To activate your product you need to provide the activation request key. To retrieve it instantiate a NetPhpRuntime object and call the ActivationGetCode() method:

$runtime = new \NetPhp\Core\NetPhpRuntime();
$runtime->Initialize();

$license_path = 'd:\\netphp\\license.txt'; 
$runtime->ActivationLicenseInitialize($license_path);

echo $runtime->ActivationGetCode();

Once you get your activation key, you can activate your product by calling the ActivationSetKey() and ActivationValid() methods:

$key = 'THIS IS THE ACTIVATION KEY';

$runtime = new \NetPhp\Core\NetPhpRuntime();
$runtime->Initialize();

$license_path = 'd:\\netphp\\license.txt'; 
$runtime->ActivationLicenseInitialize($license_path);

$runtime->ActivationSetKey($key);
echo $runtime->ActivationValid();

Once activated make sure that anytime that you create your NetPhpRuntime you Initialize the licensing system by calling ActivationLicenseInitialize().

The netphp-sample: a  step by step example

You can find a full working example of how to consume NetPhp from the sample project in Github.

https://github.com/david-garcia-garcia/netphp-sample

All the code used in this user guide can be found on the example project.

Debugging .Net code and PHP code in parallel

With NetPhp you are able to debug your .Net binary at the same time that it is being consumed from your PHP application, all from Visual Studio. To do so you must ensure that the binary being debugged matches the binary location registered in NetPhp, and use the "Debug -> Attach to process" option. In the following screenshot you can see the PHP application on the left, and the .Net binary on the right, all being debugged at the same time.

 

 

Usage Examples

These are other articles showing off how to use NetPhp on different scenarios:

Upgrading from 1.x and making both 1.x and 2.x work on the same server/machine

If were previously using the 1.x version of NetPhp and then register the 2.x version of NetPhp binary you will have a COM name colision, and when trying to instantiate these classes the system will not know wether to use the 1.x or 2.x version.

To solve that issue you can do 2 things.

If you plan to only use one version of the library, completely unregister the previous one so that it won't show up on OleView anymore.

Another possiblity - and if you plan to deploy 1.x and 2.x application on the same machine - is to use the CLSID of the component instead of the friendly name.

For NetPhp 1.x you should alter the static COM class names like this:

\NetPhp\Core\Constants::$MW_CLASS = '{1C9CB08D-9115-4AC4-8B31-E8859594AC59}';
\NetPhp\Core\Constants::$MWU_CLASS = '{BE754A07-6958-468E-B611-84E8F3FEE6A1}';

And for 2.x use this:

$runtime = new \NetPhp\Core\NetPhpRuntime('COM', '{2BF990C8-2680-474D-BDB4-65EEAEE0015F}');

Troubleshooting

Error: Uncaught exception 'com_exception' with message 'Failed to create COM object `

When you see this issue try to tweak the Application Pool settings:

Also make sure that you have properly setup fastcgi impresonation in your PHP.ini:

  • Set fastcgi.impersonate = 1.
    • FastCGI under IIS supports the ability to impersonate security tokens of the calling client. This allows IIS to define the security context that the request runs under.
  • Set cgi.fix_pathinfo=1.
    • cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI. Previously, PHP behavior was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not define PATH_INFO. For more information about PATH_INFO, see the cgi specifications. Setting this value to 1 will cause PHP CGI to fix its paths to conform to the specifications.
  • Set cgi.force_redirect = 0.

Error: Method not found: 'System.String System.String.Format(System.IFormatProvider, System.String, System.Object)

The compiled binaries use a String.Format override that is specific to the .Net 4.6 framework. We will try to target a lower version of the framework for CLR4 on the next release.

Try to update to the .Net framework 4.6.

http://stackoverflow.com/questions/30558827/method-not-found-system-string-system-string-formatsystem-iformatprovider-sy

Blocked assemblies

When you download a dll from the internet (such as an e-mail attachment or other untrusted source) Windows will mark this dll file as untrusted and you must unlock it manually.

To see if you assemblies are blocked, right click on the dll file. If blocked, use the ""Unblock" button to unblock the file.

Working with GIT

If you are using GIT as source control and your DLL files will be stored there make sure that you tell GIT to treat them as binary files, in .gitattributes:

*.dll     binary
*.swf     binary

If you don't, NetPhp will stop loading the assemblies - because they are corrupted - and the error message is not friendly or helpful.

Add new comment

By: root Friday, October 23, 2015 - 15:48