I spent a little time looking at this sample code and thinking about the questions we discussed on the phone yesterday afternoon. I have seen a number of questions posted to this newsgroup that show that you are not alone in trying to get started and to get a handle on COM, so I thought others might benefit from this overview too. This will not subsitute for a good reading of the COM specification (http://www.microsoft.com/Com/resources/comdocs.asp) and writing a COMponent from scratch (no not using ATL COM Appwizard - that does not count, but a "from scratch component" or perhaps using Don Box's (the COM developer and instructor who I hold in very high regard) Yet Another COM Libracy (YACL) http://www.developmentor.com/dbox/yacl.htm). Nor is it a substitute for the documentation inprovments we need to put in to support the COM capabilities in PHP. But perhaps it is a start....
COM is a technology which allows the reuse of code written in any language (by any language) using a standard calling convention and hiding behind APIs the implementation details (such as what machine the COMponent is stored on and the executable which houses it). It can be thought of as a super Remote Procedure Call (RPC) mechanism with some basic Object roots. It separates Implementation from Interface (I do use C++ and like it a lot, but I dislike that (unless great care it taken to separate them) the interface and implementation are bound up in the same header file). The disadvantage of this is that if I add a new private member to the class devinitions, all clients must be recompiled and relinked. Certainly it is possible to define dataless classes (except perhaps a pointer to another class or structure which holds the data) it is not the "common use" of C++. COM encourages versioning, separation of implementation from interface and hiding the implementation details such as executable location and the language it was written in. Of course there are other technologies with similar goals. In fact I am not here to advocate COM or to critique it's weeknesses. Nor do I wish to start a religeous war on the subject. So to clarify - I am not saying that Corba is bad or that C++ is evil; I am only stating that COM is well suited to building reusable components (objects) to be glued together with scripting languages such as PHP, Perl, VB, ... And other technologies can be used to do the same. Where to begin? How about we instantiate a component and then analyse the call and the uses using PHP syntax. $foo = new COM("MyClass.MyObject"); might (for example) load a DLL into the address space of PHP (In Process), or communicate with an executable on the same machine (Local) or (load a proxy stub into the address space of PHP and) result in a DCOM communication with a Unix host running a version of DCOM for Unix (Remote). Let's take this call apart. $foo is a PHP wrapper around a COM IDispatch pointer (IDsipatch is a well defined Interface which is Implememted by many component builders - i.e. many components define an implementaion IDispatch). IDispatch iteslf defines a number of methods: Perhaps the two most interesting are: GetIdOfNames(); // Get me a list of all the names, IDs and parameters that make up all the methods and properties of the object we are implementing Invoke(); // Given the definition of the methods and properties as described by GetIdsOfNames(), invoke the numbered function passing the attached arguments (each as VARIANTs - see definition later) and return any arguments chanegd by the call. How does PHP get an IDispatch interface? The IDispatch pointer is retrieved by a call to CoCreateInstance() or MkParseDisplayName (COM APIs). The "MyClass.MyObject" string is called a ProgId. This is a human readable name for a component and is not guaranteed to be unique in the Universe. i.e. two programmers might pick the same ProgIds for their components, although in practice I have yet to see this happen, in theory it could. The COM code in PHP takes this string (which may be a ProgId (e.g. "Word.Application"), a GUID string (e.g. {00000010-0000-0010-8000-00AA006D2EA4}) or a Moniker (e.g. "IIS://LocalHost/w3svc") (Support for the latter two has been added in 4.07). The ProgId or GUID string is mapped to a numeric GUID using ClsidFromString() (Another COM API) which either builds a GUID from the string or looks it up in the registry. HKEY_CLASSES_ROOT/<ProgId>/ClassId is a GUID string (like above) which can be converted into a 140 bit number which is said to be unique in the universe (a bold claim given the vastness of the universe). This ClassID is used to find the implementation HKEY_CLASSES_ROOT/CLSID/<GUIDSTRING>/The resulting GUID is passed to CoCreateInstance to retrieve the IDispatch pointer. A Moniker is passed to MkParseDisplayName() which retrieves the IDispatch interface. The GUID string and ProgId formats are equivalent to the VB CreateObject() call and the Moniker style is equivalent to the VB GetObject() call. The result is an IDispatch pointer which may be used to call properties and methods on the underlying object. This is totally invisible to the PHP coder as it is wrapped up and returned to be used like any other native PHP object (e.g. string, or class, or array). One more point about ProgIds. Often there is a version dependant and a version independant ProgId. e.g. Word.Application and Word.APplication.9. Use the former if you just want the object and don't care about version, use the latter if you need a specific version of an interface. OK, so what COMponents are out there to use? Most of the office suite have interfaces. Word.Application, Excel.Application, etc. Microsoft's new ADSI specification is all COM based. However not all COM is by definition accessible through the PHP automation interface. IDispatch is not the only well defined interface in COM (e.g. all objects are derived from IUnknown which is not directly accessible from PHP). There are other (well defined) interfaces such as IOleObject and IPersistFile which are required for visual editing and compound document support in graphical applications. I will confine this dicussion to IDispatch and the way it pertains to PHP (and other scripting languages). FOr example the Windows Task Scheduler is implemented entirely in COM and can only be accessed from C/C++ as it provides no ITypeLib (that I could fine) or IDispatch interfaces to work with. I wrote three simple IDispatch derived interfaces to wrap the WTS capabilities and so provide this interface to PHP and Perl for the project I am currently developing for my company. ITypeLib? Another "well defined" interface which (I think) was designed to replace IDispatch. The idea was to embed a full description of the Interface, its methods and properties and any enumerated types it defined into an easy to use interface. PHP (4.07) supports both ITypeLib and IDispatch interfaces. Enumerated types in the type library (a resource attached to the implementation executable) are loaded when the Interface is instantiated, thus making them available for use in method calls just as they are in VB. See the example below. OK so how do I use this IDispatch PHP wrapper returned from new COM()? You need to know the methods and properties on that object. How do you find these? Very likely your Windows box had hundreds of these compoents. Some with Automation interfaces and some not. If you have the OLE Viewer installed (http://www.microsoft.com/Com/resources/oleview.asp) it can be used to take apart the type libraries and show you the interfaces, methods, enumerated types etc. However this is really no substitute for real documentation. MSDN documents many of these interfaces (but not all), but it has the unfortunate bias for Visual Basic and it is not always easy to understand the mappings between VB and PHP. In particular, PHP does not support named arguments (although I hear there are discussions of default arguments and named arguments for the Zend 2.0 engine), nor does it support the following syntax: $foo->bar("some string")->baz; # rather you must $tmp = $foo->bar("SOme String"); $tmp->baz; This has been addressed in the current Zend 2.0 spec (I understand). In this example $foo is a PHP COM object, bar() is a method call and baz is a property. Further passing arguments by reference requires that and intervining VARIANT be created. VARIANT? A VARIANT is a well defined type in COM to allow any type to be passed in any argument to a method or returned from a property or method. Thus (under the hood in the PHP COM code) every string, integer, floating point number, array, ... must first be mapped to a VARIANT and then passed to the Invoke() method of the IDispatch interface. A variant is basically a "C" union with a type designation and either direct data or a pointer to data. Any methods requiring a pass by reference argument require that you build and pass in a VARIANT. $Vrows = new VARIANT(0, VT_I4|VT_BYREF); # Creates a PHP Variant object which is a reference to a 32 bit ineteger with an initial value of zero. Let's look at a database example (I am not much of a database guy, but here goes) Assume that you have a Microsoft Access database called c:\data\foobar.mdb (and ODBC Jet drivers and MDAC (Jet and MDAC are available on the Microsoft Web site if you do not have them)) $DB = new COM("ADODB.Connection"); # Access the ADO COnnection object and build a COM IDispatch wrapper called $DB $ConnectString = "DBQ=C:/data/foobar.mdb;DRIVER=Microsoft Access Driver (*.mdb)"; # I hate using \\ everywhere $ConnectString = ereg_replace("/", "\\", $ConnectString); $DB->Open($ConnectString, "sa", ""); # OK we now have access to the database and can send out queries $SQLString = "SELECT field1, field2, field3 FROM TheTable WHERE field1='some condition' AND field3 > \"#20011003#\" ORDER BY field3 DESC"; $Vrows = new VARIANT(0, VT_I4|VT_BYREF); $RecordSet = $DB->Execute($SQLString, $Vrows); Hold everything! What is happening here? I built up a an SQL query and passed it to the ADO COnnection object and it passed back ANOTHER IDispatch wrapped object called a RecordSet. So now I have a "collection" (but not a proper COM collection - more later) of records which match the selection criteria in teh SQL code (assuming I have a Table named "TheTable" containing fields (field1, field2, and field3 in the MDB file). The VARIANT is used to retrieve the number of rows affected by the SQL query - note it is a pass by reference argument to a method. So we iterate over the RecordSet thus: while (!$RecordSet->EOF) # EOF is a property, so needs no "()" method call syntax { $field1 = $RecordSet->Fields("field1"); $field2 = $RecordSet->Fields("field2"); $field3 = $RecordSet->Fields("field3"); # Because $RecordSet->Fields("field3")->Value is not syntactically valid in PHP4 print "Field1=" . $field1->Value . " Field2=" . $field1->Value . " Field3 =" . $field3->Value . "<BR>"; $RecordSet->MoveNext(); # Is a method and so $RecordSet->MoveNext; will not work. } $RecordSet->Close(); $DB->Close(); So there you have an OLEDB example that you can try out for yourself. Note that nowhere in this code did I use com_invoke() or any of the functions defined in the COM man pages on php.net. I am sure it is possible to access COM this way (and perhaps there are even times when it is necessary - I hear that com_set() is needed in some cases) but accessing COM this way has served me well and I intend to continue to use this syntax. OK - what about the Word example you sent. Lets look at it again. <?php $word=new COM("Word.Application.9") or die("Cannot start MS Word"); # This does not work on my machine because I have Ofifce XP (version 10) print "Loaded word version ($word->Version)\n"; $word->visible = 1 ; $word->Documents->Add(); $word->Selection->Typetext("This is a test"); $word->Selection->Typetext("123"); $word->Selection->Find->ClearFormatting; $word->Selection->Find->Text = "test"; ##<-## # PHP 4.07-dev gets beyond this for me $word->Selection->Find->Forward = False; $word->Selection->Find->Wrap = wdFindContinue; # Note the use of an enumerated constant here. $word->Selection->Collapse->Direction=wdCollapseEnd; # Simply fails because Selection has ne Collapse property $word->Selection->Find->Replacement->Text="rest"; $word->Selection->Find->Execute; # Fails because Execute is a method (Execute()) but it requires arguments ?> So let's look at two possible (working) alternatives: <?php $word=new COM("Word.Application") or die("Cannot start MS Word") print "Loaded word version ($word->Version)\n"; $word->visible = 1 ; $word->Documents->Add(); $word->Selection->Typetext("This is a test"); $word->Selection->Typetext("123"); $word->Selection->Find->ClearFormatting(); $word->Selection->Find->Text = "test"; $word->Selection->Find->Forward = True; $word->Selection->Find->Wrap = wdFindContinue; $word->Selection->Find->Replacement->ClearFormatting(); $word->Selection->Find->Replacement->Text="rest"; $word->Selection->Find->Execute(); $word->Selection->Typetext("rest"); ?> Here I tried to use the Execute() method, but it only highlighted the word "test" because there is no way (other than an argument to Execute() to pass in the wdReplaceAll constant needed to tell it to do the replacement. So I just cheated and typed over the selection. <?php $word=new COM("Word.Application") or die("Cannot start MS Word"); print "Loaded word version ($word->Version)\n"; $word->visible = 1 ; $word->Documents->Add(); $word->Selection->Typetext("This is a test"); $word->Selection->Typetext(" 123"); $word->Selection->Find->ClearFormatting(); $word->Selection->Find->Execute("test", false, false, false, false, false, true, true, false ,"rest", wdReplaceAll); ?> In this example, I actually pass all of the replacement arguments in the Execute call. The MSDN docs for this use named arguments something like: with selection .find.execute Text:="test", ReplaceText:="rest", Replace:=wdReplaceAll Since PHP does not (yet) support named arguments (or empty arguments) you have to call the method filling in all the peices that you might have allowed to default in VB. Com COllections? What about ADSI? Let's tackle these together in a single example. Warning - this is PHP 4.07 and later only. Both Monikers and COM collection aupport are new to 4.07 $Services = new COM("winmgmts:{impersonationLevel=impersonate}") or die("glump"); $NetworkAdapters = $Services->InstancesOf('Win32_NetworkAdapterConfiguration', wbemFlagBidirectional); # $NetworkAdapters is a COM collection. This means that it implements the IEnumVARIANT interface $NetworkAdapterArray = $NetworkAdapters->Next($NetworkAdapters->{'Count'}); # This is one way to grab everything in a collection # A collection defines methods, Next(), Count(), and Reset() # Either put it all in an array and foreach it, or ->Next() it one at a time foreach ($NetworkAdapterArray as $NetworkAdapter) { # $NetworkAdapter is a PHP COM IDispatch wrapper just like we used above. $NetworkAdapterSet->Reset; echo $NetworkAdapter->{'Caption'} . "<BR>"; $IpAddressPtr = $NetworkAdapter->IpAddress; # This property returns an array of IP Addresses bound to the adapter $NetMaskPtr = $NetworkAdapter->IpSubnet; # This property returns an array of IP Masks one each for the IP addresses $DefaultGatewayPtr = $NetworkAdapter->DefaultIPGateway; $GatewayCostMetricPtr = $NetworkAdapter->GatewayCostMetric; if (count($IpAddressPtr) > 0) { $IpAddress = $IpAddressPtr; } else { $IpAddress[0] = "0.0.0.0"; } if (count($NetMaskPtr) > 0) { $NetMask = $NetMaskPtr; } else { $NetMask[0] = "0.0.0.0"; } if (count($DefaultGatewayPtr) > 0) { $DefaultGateway = $DefaultGatewayPtr; } else { $DefaultGateway[0] = "0.0.0.0"; } if (count($GatewayCostMetricPtr) > 0) { $GatewayCostMetric = $GatewayCostMetricPtr; } else { $GatewayCostMetric[0] = 0; } $count = count($IpAddress); for ($ii=0;$ii<$count;$ii++) { print $NetworkAdapter->ServiceName . (($count > 1) ? ":$ii" : ""); print "\tLink ecap: HWaddr " . $NetworkAdapter->MACAddress . "\n"; print "\tinet addr:" . $IpAddress[$ii] . " Mask:" . $NetMask[$ii] . "\n"; print "\tMetric:$GatewayCostMetric[$ii]\n"; } if ( $NetworkAdapter->DHCPEnabled) { print "\tDHCP Enabled:\n"; $Date = $NetworkAdapter->DHCPLeaseObtained; $Date = FormatCOMDate($Date); print "\tLease Obtained: " . $Date . "\n"; $Date = $NetworkAdapter->DHCPLeaseExpires; $Date = FormatCOMDate($Date); print "\tLease Expires: " . $Date . "\n"; print "\tDHCP Server: " . $NetworkAdapter->DHCPServer . "\n"; } if ($NetworkAdapter->DNSDomain) { print "Domain: " . $NetworkAdapter->DNSDomain . "\n"; } if ($NetworkAdapter->DNSHostname) { print "Hostname: " . $NetworkAdapter->DNSHostname . "\n"; } print "\n"; } I hope this has been useful. It certainly took me longer to write that I had planned. I have tried to make it as technically accurate as I know how, but I make no claims about being a COM expert and there may be minor abuses of the "correct" COM and object phraseology; and of couse my apologies to the English Language for it terrible abuse in this e-mail. Alan. ----- Original Message ----- From: "Bob Kaehms" <[EMAIL PROTECTED]> To: <[EMAIL PROTECTED]> Sent: Wednesday, October 03, 2001 13:57 Subject: [PHP-WIN] More COM questions... > I've been playing with COM and PHP for quite a while now. I have some > fairly basic scripts working, but can't seem to take advantage of MS Word > for more than the most basic open/write/save operations. I've been using > the VB help and record macro capabilities of word to attempt to get some > things working in PHP. > > I'm using W98, apache 1.3.12, and PHP as a DSO. Word is Word 2000 (9.0). > > Here is a simple example of a script that tries to generate some text, and then > do a search and replace to edit a field. > > <?php > $word=new COM("Word.Application.9") or die("Cannot start MS Word"); > print "Loaded word version ($word->Version)\n"; > $word->visible = 1 ; > $word->Documents->Add(); > > $word->Selection->Typetext("This is a test"); > $word->Selection->Typetext("123"); > $word->Selection->Find->ClearFormatting; > $word->Selection->Find->Text = "test"; ##<-## > $word->Selection->Find->Forward = False; > $word->Selection->Find->Wrap = wdFindContinue; > $word->Selection->Collapse->Direction=wdCollapseEnd; > $word->Selection->Find->Replacement->Text="rest"; > $word->Selection->Find->Execute; > ?> > > I've created this by first recording a macro, and then trying to convert. > After commenting out various lines, it seems to hang on ##<-## > It prints out the first two typetext lines, and then a third "test" at the > end of the document, in highlighted form (it does not just highlight the > first instance of "test". > > 3 Questions: > > 1) Is this revision of OS/MSword/Apache/PHP stable? > 2) Does the above syntax look correct, or have I missed something? > 3) Is it possible to pass variables to Word macros via PHP and > drive the entire application from the other side? > > > Note that I haven't seemed to have any problems with Excel. > > > Thanks > > --Bob > > > > -- > PHP Windows Mailing List (http://www.php.net/) > To unsubscribe, e-mail: [EMAIL PROTECTED] > For additional commands, e-mail: [EMAIL PROTECTED] > To contact the list administrators, e-mail: [EMAIL PROTECTED] > > -- PHP Windows Mailing List (http://www.php.net/) To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED] To contact the list administrators, e-mail: [EMAIL PROTECTED]